From 6a0ca66e171f68e366b5a1f1c637d093f6356a2b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 09:57:01 +0500 Subject: [PATCH 001/172] docs(.design): add overhaul kickoff docs --- .design/BACKEND-REFINEMENTS.md | 166 ++++++ .design/DECISIONS.md | 127 +++++ .design/SCREEN-INVENTORY.md | 90 ++++ .design/UI-SPEC.md | 950 +++++++++++++++++++++++++++++++++ .design/UX-ARCHITECTURE.md | 817 ++++++++++++++++++++++++++++ .design/UX-AUDIT.md | 325 +++++++++++ 6 files changed, 2475 insertions(+) create mode 100644 .design/BACKEND-REFINEMENTS.md create mode 100644 .design/DECISIONS.md create mode 100644 .design/SCREEN-INVENTORY.md create mode 100644 .design/UI-SPEC.md create mode 100644 .design/UX-ARCHITECTURE.md create mode 100644 .design/UX-AUDIT.md diff --git a/.design/BACKEND-REFINEMENTS.md b/.design/BACKEND-REFINEMENTS.md new file mode 100644 index 000000000..e0f2f4ae6 --- /dev/null +++ b/.design/BACKEND-REFINEMENTS.md @@ -0,0 +1,166 @@ +# Backend Refinements — Design Overhaul + +Hand this to your backend coding agent. Every item is a field/endpoint/storage gap surfaced during design handoff analysis. Frontend ships with fallbacks for unfulfilled items, but full design fidelity requires these. + +Origin: `~/Downloads/handoff 4/DESIGN.md` §14 + agent specs (`.design/UX-AUDIT.md`, `UX-ARCHITECTURE.md`, `UI-SPEC.md`). + +## Priority legend + +- **P0** — Blocks a core primitive from rendering correctly. Frontend uses neutral fallback. +- **P1** — Enables a richer signal. Frontend renders partial state until backend lands. +- **P2** — Nice-to-have polish. + +--- + +## R1 — `trendingScore` (P0) + +**Field:** `Repository.trendingScore: Float?` (or `Int?` rank position) +**Where used:** Trending section on Home (DESIGN.md §9.3 / §10.1). Drives the `#1 / #2 / #3` rank chip. +**Today:** Sample API returned `null`. Frontend currently uses a local affinity-`score` proxy in `HomeRepositoryImpl.kt:571-604`. +**Need:** Server-side ranking (e.g. release count × stars × freshness × velocity in last 7d). Stable per request; cache TTL ≥ 1h. Position (1, 2, 3…) is preferred over percentage to honor DESIGN.md §11 "no invented percentages." + +## R2 — `popularityScore` (P0) + +**Field:** `Repository.popularityScore: Int?` (rank position) +**Where used:** "Most popular" section on Home. Drives the rank numbers (Fraunces italic, opacity 0.55). +**Today:** Sample returned `null`. Frontend sorts by `stargazersCount` locally. +**Need:** Server-side all-time rank. Same caching as trending. Independent of time window. + +## R3 — Per-app accent color (P1) + +**Field:** `Repository.accent: { hex: String, lightTint: String, darkTintAlpha: Float }?` +**Where used:** Lead card bg tint, FreshnessRing outer, install-panel bloom, Apps row tinted leading icon. +**Today:** No field. Frontend will resolve in this order (per UX-Architecture §7): backend → topic-derived → language-derived → blue fallback. +**Backend opportunity:** Run a color-thief / palette extraction on `avatarUrl` once at index time; persist `{ hex, lightTint, darkTintAlpha }`. Saves CPU on every device + stays consistent across devices. **Deferred per D5** — frontend ships topic/language fallback first. Re-evaluate after overhaul lands. + +## R4 — Sub-day release recency (P2) + +**Field:** `Release.publishedAtUtcSeconds: Long` (in addition to current `publishedAt: Instant`) +**Where used:** FreshnessRing for releases <24h. Currently rounds to "1 day ago"; design wants "5 hours ago" granularity for the Hot bucket. +**Today:** `publishedAt: Instant` exists — frontend can derive seconds locally. **No backend change required if backend already returns ISO-8601 with seconds precision.** Confirm. + +## R5 — Maintenance state (commit recency) (P1) + +**Field:** `Repository.lastCommitAt: Instant?` or `Repository.daysSinceLastCommit: Int?` +**Where used:** Heartbeat animation period (active 1.4s / recent 2.4s / quiet 4.2s / dormant none — per DESIGN.md §6.1). +**Today:** Repository has `updatedAt` (last metadata update) but that's not commit recency. GitHub API exposes `pushed_at` — pipe through. +**Need:** Backend persists + serves `pushed_at` (default branch HEAD timestamp) per repo. Refresh hourly or on release. + +## R6 — Mirror health for `SignalBars` (P0) + +**Field:** Per-mirror `health: { latencyMs: Int, lossPercent: Float, lastCheckedAt: Instant }` +**Where used:** Mirror picker — `SignalBars` (4 ascending bars, WiFi-style) shows mirror quality. +**Today:** GHS pings mirrors client-side (existing `AutoSuggestMirror` logic). Lives on device, not in `MirrorsRepository`. +**Need (backend):** Optional — backend-aggregated health metrics across users would improve cold-start UX. Not required if client-side latency probe stays. + +## R7 — Expected APK signing fingerprint (P0) + +**Field:** `Repository.signingFingerprint: { sha256: String, source: "first_install" | "publisher_declared" }?` +**Where used:** Wax-seal trust card (DESIGN.md §7.8). Cracked-seal red state requires comparing installed APK fingerprint to expected. +**Today:** No "expected" fingerprint stored anywhere. Cracked-seal state can never fire. +**Need:** +- On-device: persist fingerprint of first successful install per `InstalledApp` (Room schema addition). Subsequent installs compare against this. +- Optional backend: publisher-declared fingerprint (maintainer registers via dashboard) — out of scope for this overhaul unless backend wants it. + +## R8 — Translation cache persistence (P2) + +**Field:** Backend-side translation cache keyed by `(target_lang, version_tag, repo_id)`. +**Where used:** Inner Detail screen language toggle. Hot path: user clicks "Translate to es", waits for provider response. +**Today:** Client-side in-memory cache only (kills on app restart). +**Need:** Either (a) backend caches translations and serves them via `/v1/translate/cache` so cold-start re-opens are instant, or (b) client persists to Room (already aligned with KSafe stack — frontend can do this). +**Recommendation:** Client-side persistence to Room is simpler. Skip backend cache unless multi-device sync is desired. + +## R9 — `permissionRisk` summary (P1) + +**Field:** `Repository.permissionRisk: "low" | "moderate" | "high"?` +**Where used:** Vital signs grid (Permissions tile) on Detail. `PermDot` color (green/amber/red). +**Today:** Frontend computes locally from APK Inspect results (Android only). Desktop can't show — needs backend. +**Need (backend):** When scanning a release's APK assets, classify by protection-level groups (Android docs: normal/signature/dangerous). Cache per `(repoId, releaseTag)`. Required for Desktop parity. + +## R10 — `licensePosture` from SPDX (P1) + +**Field:** `Repository.licensePosture: "copyleft" | "permissive" | "unknown"` +**Where used:** `LicensePosture` glyph (Filled © tile vs dashed · tile). +**Today:** `Repository.license` is a free-text string today. Frontend has hardcoded mapping in tokens.json (`licenses.copyleft`, `licenses.permissive`). +**Need:** Backend normalizes to SPDX identifier on indexing. Frontend uses the tokens.json map. **No backend change needed if license already SPDX.** + +## R11 — `downloads` field (P1) + +**Field:** `Repository.totalDownloads: Long` (sum across all release assets) +**Where used:** `DownloadWeight` primitive (radius is log10(downloads)) + meta line "4.8M dl" on Detail. +**Today:** Existing Forgejo path aggregates client-side. GitHub doesn't expose easily — current GHS backend likely already proxies. +**Need:** Confirm `totalDownloads` populated for both GitHub + Forgejo paths. + +## R12 — `topics` field (P1) + +**Field:** `Repository.topics: List` +**Where used:** TopicGlyph row (up to 3, mapped via `tokens.json.topicGlyphs.supported` + `topicAliases`). +**Today:** GitHub API returns topics; confirm backend forwards them. Forgejo `/repos/topics` endpoint exists. +**Need:** Both branches expose `topics` consistently. If topic count is high, prioritize ones in our supported map. + +## R13 — `pushedAt` vs `updatedAt` clarity + +**Field naming:** Distinguish "last metadata change" (`updatedAt`) from "last commit" (`pushedAt`). +**Today:** `Repository.updatedAt` ambiguous in our API. +**Need:** Rename or split — UX-Audit flagged this as confusing. + +## R14 — Backend support for accent override on a per-repo basis (P2) + +**Field:** Allow maintainer-published `accent: {hex, lightTint, darkTintAlpha}` overrides. +**Where:** Some apps have brand colors that don't match topic/language fallback. Maintainer should be able to opt in. +**Today:** No mechanism. +**Need:** Out of scope unless backend wants. Frontend's fallback chain (topic → language → blue) is acceptable for v1. + +--- + +## Endpoint additions needed + +| Endpoint | Purpose | Priority | +|----------|---------|----------| +| `GET /v1/repo/{owner}/{repo}/trending-rank` | Returns `{ rank: Int, lastComputedAt: Instant }` if repo in top-100 trending. Else 404. | P0 | +| `GET /v1/repo/{owner}/{repo}/popularity-rank` | Same shape, all-time. | P0 | +| `GET /v1/repo/{owner}/{repo}/permissions-summary` | Returns `{ posture: low|moderate|high, dangerous: List, sensitive: List }` for latest release. | P1 | +| `GET /v1/translate/cache?lang=&version=&repo=` | Returns cached translation or 204. Skip if going Room-only. | P2 | + +--- + +## Data model summary + +Backend `Repository` payload after refinements should have these new/clarified fields: + +```jsonc +{ + "id": 123, + "owner": "...", + "name": "...", + // ... existing fields ... + + // NEW or clarified + "trendingScore": 1, // rank position in trending (nullable) + "popularityScore": 47, // rank position all-time (nullable) + "pushedAt": "2026-05-20T...",// last commit (DISTINCT from updatedAt) + "totalDownloads": 4843201, + "topics": ["self-hosted", "photos"], + "licensePosture": "copyleft", // server-normalized from SPDX + "permissionRisk": "moderate", // optional pre-computed summary + "accent": null // deferred — null until D5 reversed +} +``` + +--- + +## Frontend fallback behavior (what ships without backend changes) + +Per UI-SPEC.md §5 Data Honesty Audit Hooks: + +- `trendingScore` null → drop rank chip; sort by local affinity +- `popularityScore` null → sort by stars; drop rank number +- `accent` null → topic → language → blue fallback +- `pushedAt` null → use `updatedAt` (with caveat in Heartbeat label) +- `mirrorHealth` null → client-side ping +- `signingFingerprint` not stored locally → Wax-seal stays in "Unsigned" open state forever +- `permissionRisk` null → derive on Android via APK Inspect; show "—" on Desktop +- `licensePosture` SPDX missing → "unknown" → dashed neutral tile +- `topics` missing → no TopicGlyph row + +Nothing fabricated. Missing primitives just don't render. diff --git a/.design/DECISIONS.md b/.design/DECISIONS.md new file mode 100644 index 000000000..5f7290240 --- /dev/null +++ b/.design/DECISIONS.md @@ -0,0 +1,127 @@ +# Design Overhaul Decisions + +Locked decisions before Phase 0. Source: maintainer answers during kickoff session (2026-05-21). + +## D1 — Bottom nav 5 → 4 + +Drop **Tweaks** tab from bottom nav. Reachable via Profile → Settings (already wired). +Keep Home / Search / Apps / Favourites. +Override: handoff's 3-tab + Search-FAB rejected as too disruptive to muscle memory. + +## D2 — Noto fonts + +Bundle **Latin-only** Fraunces, Inter Tight, JetBrains Mono with the app. +Non-Latin scripts (CJK / Devanagari / Arabic / Hebrew / Thai etc.) fall back to system fonts. +Modern Android / macOS / Windows ship Noto-like coverage; Linux may show tofu (acceptable risk). +Total bundled font weight target: ≤ 4 MB across all three families. + +## D3 — AppTheme migration map + +Legacy `AppTheme` enum → new `Palette`: +- `OCEAN` → `NORD` +- `SLATE` → `NORD` +- `PURPLE` → `PLUM` +- `FOREST` → `FOREST` +- `AMBER` → `CREAM` + +One-time migration on first launch after upgrade. Surface a non-blocking banner ("Themes refreshed — try the new palettes in Tweaks") via a new one-shot flag in `TweaksRepository`. + +## D4 — Wonky-squircle scope + +Full DESIGN.md coverage: hero CTAs, lead cards, search input, bottom sheets, confirm dialogs, toasts, device-code box, and any "wonky" call-out in the handoff. Implement as custom `WonkySquircleShape : Shape` using `Path.arcTo` with elliptical bounding rects. No fallback to `RoundedCornerShape`; the asymmetric character is brand-defining. + +## D5 — Sponsor cut + +Cut `SponsorScreen` entirely. Remove: +- Route `GithubStoreGraph.SponsorScreen` +- Profile sponsor row (`feature/profile/.../Options.kt:114`) +- Nav wiring in `AppNavigation.kt` +- Any deep links / strings + +Aligns with MIGRATION.md "no donations in UI." + +## D6 — Desktop two-pane Library + +Build during **Phase 4**. Desktop `feature/apps/` adopts list (380dp) + detail (660dp) two-pane per DESIGN.md §8.3. Android remains single-pane → full-screen detail. +Existing single-pane code path retained behind the same composable signature; platform branch picks layout. + +## D7 — AMOLED mode + +`ThemeMode` enum = `LIGHT` / `DARK` / `AMOLED` / `SYSTEM`. AMOLED is a Dark sub-mode (resolves to Dark when `SYSTEM = dark`). All 4 palettes ship an AMOLED variant — pure-black `bg` (#000) + dimmed `surface` (lift one notch up). Adds ~30% token surface but contained to `Tokens.kt`. + +## D8 — Visual reference + +Open `desktop-app.html` / `mobile-app.html` / `design-system.html` **per phase** as side-by-side reference. Not opened upfront en-masse. + +--- + +## Carry-over from agent specs (no decision needed — accepted as written) + +- Tokens delivered as hand-written `Tokens.kt`, not Gradle codegen (UX-Architect §1). +- Single `core/presentation/.../vocabulary/` module hosts all primitives (UX-Architect §8). +- Per-app accent at UI mapper layer, not domain model (UX-Architect §7). +- `GhsTheme` composable wraps `MaterialExpressiveTheme` + 6 composition locals (UX-Architect §2). +- Build order: Phase 0 → 8 per UX-Architecture sequencing. +- Vocabulary uses `ImageVector.Builder` + `Canvas`; Material Symbols painters where applicable (UX-Architect §9). + +## D9 — Coverage policy + +Design handoff covers only some screens (Home, Library, Detail incl. Inner About/What's-new, Search, APK Inspect, overlay surfaces, Tweaks fragments). Many screens are uncovered. Policy: **plan each uncovered screen before implementing**, extrapolating from DESIGN.md primitives + patterns + tokens. Ask clarification questions before building. + +## D10 — Motion scope (rich) + +Beyond DESIGN.md §6.2 baseline (Heartbeat + 120ms tap + palette/mode crossfade): +- Shared-element transitions (Compose `SharedTransitionLayout`) on avatar → Detail hero +- Spring physics on press / release (`spring(dampingRatio = MediumBouncy)`) +- List item enter/exit (slide 200ms) +- Parallax on Detail hero scroll +- Skeleton loaders during async fetches + +Quick, never blocking user. Spring stiffness defaults: high (300+) so transitions feel snappy. + +## D11 — Backend refinements doc + +`.design/BACKEND-REFINEMENTS.md` drafted upfront. Hand to backend coding agent in parallel while frontend builds. Frontend ships with fallbacks for any field not yet populated. + +## D12 — PR cadence + +**One mega-PR** at the end of overhaul. Commits per feature / milestone (atomic). Memory rule (commit msgs ≤10 words) applies. Each commit compiles + runs. + +## D13 — Build order + +User-directed: **Root + Navigation first**, then **core module fully**, then **feature-by-feature**. Translated to 17 phases — see `MEMORY.md` `project_design_overhaul` and the task list. + +## D15 — SponsorScreen full delete + +Delete `feature/profile/.../SponsorScreen.kt` composable, its route in `GithubStoreGraph`, nav wire in `AppNavigation`, sponsor row in `feature/profile/.../Options.kt`, and all related strings across 13 locales. No flag, no shim. + +## D16 — Apps tab renamed "Library" + +Label change only. Route name `AppsScreen` stays (no breaking change for deep links). BottomNav + Drawer label → "Library". String resource updated across 13 locales. + +## D17 — Onboarding (3 steps) + +New first-launch flow: +1. **Palette pick** — 4 Cookie swatches (Nord/Cream/Forest/Plum) + System/Light/Dark mode default +2. **Sign in (optional)** — entry point to web-OAuth or device-flow or skip +3. **Permissions (Android)** — notifications + install-from-unknown-sources prompts (skip-able) + +One-shot. Persisted via `TweaksRepository.onboardingComplete: Boolean`. Skipped on subsequent launches. Lives in `composeApp/` as app-level orchestration (no new feature module — too small). + +## D18 — Shared elements: Android only + +Use `SharedTransitionLayout` + `sharedElement` modifier (`@OptIn(ExperimentalSharedTransitionApi::class)`) on Android only. Desktop uses standard slide/fade nav transitions via Compose Navigation's `enterTransition` / `exitTransition`. Platform branch via `expect/actual` or runtime platform check. + +## D14 — Architecture skills + +Source of truth for ViewModel/State/Action/Event/Root/Screen structure, navigation, DI, error handling, testing: `~/.claude/skills/android/*`. Applied to KMP/CMP common code (most patterns platform-agnostic). + +--- + +## Deferred / explicitly out of scope for this overhaul + +- Color-thief / server-side accent derivation from avatar (UX-Architect Q6) — use topic + language fallbacks only. +- Translate-the-app (Crowdin pipeline) (UI-Designer §8). +- Material You dynamic color override (themes.md §Disallowed). +- New 5th palette (themes.md §Disallowed). +- Fake trending percentages / "Featured" curation / invented data (DESIGN.md §11). diff --git a/.design/SCREEN-INVENTORY.md b/.design/SCREEN-INVENTORY.md new file mode 100644 index 000000000..4a3d18cff --- /dev/null +++ b/.design/SCREEN-INVENTORY.md @@ -0,0 +1,90 @@ +# Screen Inventory + +Master list of every screen in GHS + its coverage by the handoff. Drives Phase 6+ work order. Uncovered screens get pre-build design pass + maintainer approval per decision D9. + +## Coverage legend + +- **FULL** — handoff has ASCII layout + spec + JSX reference for this screen +- **PARTIAL** — handoff mentions the screen but only fragments (e.g. component used, but no full layout) +- **NONE** — handoff doesn't cover; extrapolate from primitives/patterns + ask user + +## Primary routes (from `GithubStoreGraph.kt`) + +| # | Route | Module | Coverage | Handoff ref | Notes | +|---|-------|--------|----------|-------------|-------| +| 1 | `HomeScreen` | `feature/home/` | **FULL** | DESIGN.md §9.3 / §10.1 + `home.jsx` + `mobile.jsx` | Lead + Hot + Trending + Popular + From-your-stars | +| 2 | `SearchScreen(initialPlatform)` | `feature/search/` | **PARTIAL** | DESIGN.md §10.3 + `desktop.jsx` `DesktopSearchScreen` | Source toggle (GitHub/Codeberg/Custom) NOT in handoff | +| 3 | `AuthenticationScreen` | `feature/auth/` | **PARTIAL** | DESIGN.md §16.5 (full-screen sheet pattern w/ device-flow code) | Web-OAuth path + PAT path need extrapolation | +| 4 | `DetailsScreen(repoId, owner, repo, isComingFromUpdate, sourceHost)` | `feature/details/` | **FULL** | DESIGN.md §8.4 + §9.4 + `desktop.jsx` + `mobile.jsx` + `detail-inner.jsx` | Foreign-source variant needs handling | +| 5 | `DeveloperProfileScreen(username)` | `feature/dev-profile/` | **NONE** | — | Pre-build design pass | +| 6 | `ProfileScreen` | `feature/profile/` | **PARTIAL** | DESIGN.md §8.1 mentions user card + MIGRATION.md §risky areas (Connect + Business inquiries vs donate) | Need full layout pass | +| 7 | `TweaksScreen` | `feature/tweaks/` | **PARTIAL** | DESIGN.md §8.1 mentions Settings entry, `tweaks-panel.jsx` shows palette picker only | Full Tweaks UI needs design — many sub-sections (Network, Translation, Installation, Updates, etc.) | +| 8 | `FavouritesScreen` | `feature/favourites/` | **NONE** | — | Reuse list-row pattern but pre-build design pass | +| 9 | `StarredReposScreen` | `feature/starred/` | **NONE** | — | Reuse list-row pattern but pre-build design pass | +| 10 | `RecentlyViewedScreen` | `feature/recently-viewed/` | **NONE** | — | Reuse list-row pattern but pre-build design pass | +| 11 | `AppsScreen` (= Library) | `feature/apps/` | **FULL** | DESIGN.md §8.3 / §9.5 + `desktop.jsx` (two-pane) + `mobile.jsx` | Desktop two-pane is net-new for GHS | +| 12 | `ExternalImportScreen` | `feature/apps/` (import wizard) | **PARTIAL** | DESIGN.md §16.5 full-screen sheet pattern | Multi-step Obtainium import flow needs full design | +| 13 | `MirrorPickerScreen` | `feature/tweaks/` (mirror sub-screen) | **NONE** | tokens.json mentions `SignalBars` for mirror speed | Pre-build design pass | +| 14 | `SkippedUpdatesScreen` | `feature/tweaks/` (updates sub-screen) | **NONE** | — | Pre-build design pass | +| 15 | `HiddenRepositoriesScreen` | `feature/tweaks/` (updates sub-screen) | **NONE** | — | Pre-build design pass | +| 16 | `WhatsNewHistoryScreen` | `feature/profile/` or standalone | **NONE** | DESIGN.md §16.6 mentions What's-new sheet (per-version), not history page | Pre-build design pass | +| 17 | `AnnouncementsScreen` | `feature/profile/` or standalone | **NONE** | — | Pre-build design pass | +| 18 | `StarredPickerScreen` | `feature/apps/` (link-app wizard) | **NONE** | — | Pre-build design pass | +| 19 | `HostTokensScreen` | `feature/tweaks/` (access tokens) | **NONE** | — | Pre-build design pass (per-host PAT manager — GHS-specific) | + +## Cross-screen overlay surfaces (DESIGN.md §16) + +| Surface | Coverage | Where it shows up | +|---------|----------|-------------------| +| Bottom sheet (asset picker, install variant, source) | **FULL** §16.1 | Details install panel, link-app sheet | +| Confirm dialog (sign out, uninstall, clear cache) | **FULL** §16.2 | Profile sign-out, Apps uninstall, Tweaks clear | +| Toast (retry-after, install complete, network changed) | **FULL** §16.3 | Cross-app | +| Diagnostics card (Send feedback) | **FULL** §16.4 | Tweaks → Feedback (current `FeedbackBottomSheet`) | +| Full-screen sheet (OAuth, PAT, Imports wizard) | **FULL** §16.5 | Auth, ExternalImport | +| What's-new sheet (per-version one-shot) | **FULL** §16.6 | First launch after upgrade (existing `WhatsNewHistory` ≠ this sheet) | +| Dropdown menu (translate lang, sort, palette) | **PARTIAL** §16.7 | TranslateButton in Details, Apps sort menu | + +## Sub-components / dialogs that need design (GHS-specific, no handoff coverage) + +- **CustomForgesDialog** (Tweaks → Network → Custom forges) — list + add/remove forge hosts +- **AdvancedAppSettingsBottomSheet** (Apps → row long-press) — asset filter regex, monorepo fallback, pin variant, include pre-releases +- **LinkAppBottomSheet** (Apps → unlinked device app) — search GitHub repo + pick + link +- **ApkInspectSheet** (Details → Android only) — partial coverage in DESIGN.md §9.6 standalone APK Inspect screen +- **ReleaseAssetsPicker / VersionPicker / VersionTypePicker** (Details install panel) — pickers for asset / version / channel +- **AutoSuggestMirrorSheet** (Tweaks → Mirror auto-suggest) — locale-gated suggestion +- **FeedbackBottomSheet** (Tweaks → Feedback) — already aligned with §16.4 pattern +- **DownloadProgress / InstallProgress UI** (Details install flow) — DESIGN.md mentions inline progress but doesn't detail +- **LanguagePicker** (Details → translate) — DESIGN.md §16.7 pattern +- **TranslationControls** (Details → translate bar) — provider-aware toggle +- **PlatformSectionCard** (Details → cross-platform assets) — group APK/EXE/DMG by platform + +## Sub-screens not in `GithubStoreGraph.kt` but composed inside features + +- **APK Inspect** (full-screen, Android only) — handoff DESIGN.md §9.6 covers FULL +- **Inner Detail (About / What's-new tabs + version rail)** — handoff DESIGN.md §8.4 covers FULL +- **Onboarding** (first launch) — not currently a screen in GHS but worth considering. MIGRATION.md mentions "first-launch surfaces" +- **Empty states** (Library before scan, Search before query, Updates when none, Favourites empty) — `patterns.md` §Pattern: Empty state + +## What stays purely behavioural (no UI redesign) + +- Install flow (Shizuku / Dhizuku / Root / Default) — pickers re-styled, logic untouched +- Background update check (WorkManager) — no UI surface +- KSafe encryption layer — no UI +- Crash reporter (Desktop) — no UI +- WinGet publish workflow — CI only +- SignPath Windows signing — CI only + +## Open per-screen questions (resolve in their respective phases) + +1. **Onboarding** — build it? (Currently no formal onboarding; first launch goes straight to Home.) +2. **Profile Connect rows** — what platforms? (Mastodon, GitHub Discussions, Reddit, Discord? Maintainer pick.) +3. **Profile Business inquiries** — what data? (Email + GitHub Issues link?) +4. **WhatsNewHistory** — page or per-version sheet only? (Current GHS has a route — keep as history list.) +5. **Announcements** — backend-driven or static? (Current is backend-driven.) +6. **MirrorPickerScreen vs MirrorPickerSheet** — is full-screen still right, or move to bottom sheet? +7. **HostTokensScreen** — table-style or card-style rows? (Sensitive data; mask PAT by default.) +8. **CustomForgesDialog** — keep as dialog or promote to sub-screen? +9. **Desktop two-pane Apps** — does it apply to Favourites/Starred/Recently-viewed too? +10. **Search source toggle** — chip row above results or in filter sheet? + +These get re-asked in the relevant phase. diff --git a/.design/UI-SPEC.md b/.design/UI-SPEC.md new file mode 100644 index 000000000..c51ca44b5 --- /dev/null +++ b/.design/UI-SPEC.md @@ -0,0 +1,950 @@ +# GitHub Store — UI Build Spec (Design System Refresh) + +> Branch: `feat/design-system-refresh`. Anchored against `/Users/rainxchzed/Downloads/handoff 4/` (DESIGN.md, tokens.json, patterns.md, design-system.md, prototype `.jsx` files). All Kotlin/Compose specs target Compose Multiplatform — `commonMain` unless noted. No code yet; this is the build contract. +> +> Voice: Silent Vocabulary (80%) + Expressive moments (20%). Friendly editorial. Honest data. +> +> Token references use the literal keys from `handoff 4/tokens.json` (e.g. `palettes.nord.light.primary`, `shape.radii.card-lg`, `status.freshness.hot`). DESIGN.md citations look like `DESIGN.md §7.3`. + +--- + +## 0. Glossary & conventions + +- **T** — current palette+mode token bundle (`bg`, `surface`, `surface2`, `ink`, `ink2`, `outline`, `primary`, `tintP`, `success`, `successT`, `danger`, `dangerT`). Defined per palette × {light, dark} in `tokens.json#palettes`. +- **Status** — palette-independent semantic colors (`tokens.json#status.{freshness,wax,perm,trend}`). Same hex in every palette. +- **Accent** — per-repo brand color triplet `{c, lt, dt}` (DESIGN.md §2.4). Travels with the repo; never mutates with palette. +- **dp** — Compose `Dp` units. Compose tokens consume the integer "primary"/"secondary" pair from `tokens.json#shape.radii.*` (the `css` strings are reference only). +- **Wonky** — fully asymmetric `RoundedCornerShape` with four distinct corner radii (DESIGN.md §5.2). Implemented as `AbsoluteRoundedCornerShape(topStart=…, topEnd=…, bottomEnd=…, bottomStart=…)`. +- **Mono** — JetBrains Mono. Used **only** for technical artifacts (version tags, hashes, file sizes, package names). Never for prose. +- **Primary surface** — the most important interactive thing on the screen. One per surface (DESIGN.md §3, §7.1, design-system.md §11.1). +- **`GhsTheme`** — new theme composable (the wrapper that replaces today's `GithubStoreTheme`). Wraps `MaterialExpressiveTheme` and publishes `LocalGhsTokens`, `LocalGhsStatus`, `LocalGhsTypography`, `LocalGhsShapes`, `LocalGhsAccent`. + +Compose Multiplatform deltas vs the JSX prototypes: +- No CSS `box-shadow` strings — translate to `Modifier.shadow(elevation, shape, …)`. +- No `linear-gradient` for chrome — only for the Lead-hero radial bloom (DESIGN.md §2.5 explicitly allows it) and the Profile identity card (patterns.md §"Hero card with gradient", `tintP → surface` only). +- No `borderRadius: 'A B C D / E F G H'` 8-value strings. Compose can express that with `GenericShape`; for the wonky squircle we use a 4-corner `AbsoluteRoundedCornerShape` and accept that the second axis is symmetric. The visual delta is negligible at the sizes we use. + +--- + +## 1. Primitive catalogue (Silent Vocabulary + Expressive) + +Every primitive lives in `core/presentation/src/commonMain/.../theme/primitives/`. Public composables, no business state, no Koin. Inputs are typed pure data. + +Cross-platform rule for all primitives that handle pointer input: the **touch target** is min 48dp on Android; Desktop uses `Modifier.pointerHoverIcon(PointerIcon.Hand)` for hover-actionable primitives only (rings/dots are pure decoration). Where a primitive accepts a `tooltip: String?` Desktop uses `TooltipBox`; Android ignores it. + +### 1.1 FreshnessRing + +| Field | Value | +|---|---| +| Answers | "How recent is this release?" (DESIGN.md §4.1) | +| Inputs | `daysSinceRelease: Int?`, `size: Dp = 56.dp`, `strokeWidth: Dp = 2.5.dp`, `accent: Color? = null`, `content: @Composable BoxScope.() -> Unit` | +| Geometry | Outer arc starts at -90°, sweep = `360f * fraction`. Inner ring (full circle) drawn at `outline @ 0.35 alpha` as the unfilled track. Inset 1.5dp between ring and content. | +| State buckets | From `tokens.json#thresholds.freshness`: `hot 0–3d`, `fresh 4–30d`, `warm 31–90d`, `cool 91–365d`, `dormant >365d`. Fractions `1.00 / 0.78 / 0.55 / 0.30 / 0.12`. Colors from `status.freshness.{state}`. | +| Accent rule | When `accent != null`, the *outer* arc uses `accent.c`; the bucket color appears as a small chord segment at the tail to disambiguate. Default (no accent) uses bucket color for the whole arc. | +| States | default; loading (full ring `outline @ 0.5`, slow pulse 1.6s); null-input (no ring, content alone, no halo). | +| Behavioral rules | Always paired with an avatar inside (DESIGN.md §7.4). Never used as a button. **Never** stacked with Heartbeat in the same row (DESIGN.md §6.1). | +| Cross-platform | Compose `Canvas` draws the arc; the content slot is a `Box` accepting an image or letter avatar. | + +### 1.2 Heartbeat + +| Field | Value | +|---|---| +| Answers | "Is this project alive?" | +| Inputs | `daysSinceUpdate: Int?`, `size: Dp = 8.dp`, `showHalo: Boolean = true`, `tint: Color? = null` | +| Geometry | Center dot `size`. Halo circle scales 1.0 → 2.4 with opacity 0.45 → 0 (`tokens.json#motion.heartbeat`). | +| State buckets | From `tokens.json#thresholds.maintenance`: `active ≤1d → 1.4s`, `recent ≤7d → 2.4s`, `quiet ≤30d → 4.2s`, `dormant >30d → no animation`. | +| Colors | `active/recent → status.freshness.fresh`; `quiet → status.freshness.warm`; `dormant → status.freshness.dormant` (static, no halo). | +| States | default; dormant (no animation); reduced-motion (use static dot, regardless of bucket — read `LocalConfiguration` on Android, `Toolkit.getDefaultToolkit().getDesktopProperty("awt.dynamicLayoutSupported")` is not reliable; expose a `GhsMotionPreference` Local instead, defaulting to "full"). | +| Behavioral rules | **Never** in dense list rows alongside FreshnessRing (DESIGN.md §6.1, §15). Allowed: detail vital signs, library row when ring is suppressed, polling indicator in OAuth full-screen sheet (§16.5 — repurposed as "this flow is alive"). | +| Cross-platform | `rememberInfiniteTransition` for both desktop & android. | + +### 1.3 StarTier + +| Field | Value | +|---|---| +| Answers | "How big a deal is this?" | +| Inputs | `stars: Int`, `size: Dp = 11.dp`, `tint: Color? = null` (defaults to `T.ink`) | +| Geometry | 5 stars in a row, 1.5dp gap. Filled `tier` stars; the rest outlined at `0.35 alpha`. | +| Buckets | `tokens.json#thresholds.stars`: 0→1, 1k→2, 10k→3, 50k→4, 100k→5. | +| Numeric tail | Optional `showCount: Boolean = true` → appends count in `mono 12` (Inter Tight tabular numerals — design-system.md §5.3). Uses `CountFormatter` (already exists at `core/presentation/.../utils/CountFormatter.kt`). | +| States | default; disabled (50% alpha, used in hidden-repos screen). | + +### 1.4 WaxSeal + +| Field | Value | +|---|---| +| Answers | "Can I trust this binary?" | +| Inputs | `state: WaxState` = `Intact | Cracked | Open`, `size: Dp = 36.dp`, `fingerprint: String? = null` | +| Geometry | Octagonal stamp shape (DESIGN.md §7.8). Cracked state has a jagged centre fissure (drawn as `Path` with two angled lines + small spalls). Open state uses a `1.5dp dashed` stroke on the outer outline only. | +| Colors | `Intact → status.wax.intact (#8B4A2B)` fill, ink2 outline; `Cracked → status.wax.cracked (#B83A2C)` fill, white check that becomes a red cross; `Open → transparent fill, status.wax.open (#8E8E8E)` dashed outline. | +| States | The cracked state is the **only** place the app uses red aggressively (DESIGN.md §7.8). Must visibly scream — full `T.danger` border on the containing card and a wax-seal toast (§toast). | +| Behavioral rules | Anchored top-right of the install panel on Desktop, slightly rotated -6° for "stamped" feel (DESIGN.md §8.2/§7.8). Top of the card on Android (full width). Always paired with the signing fingerprint in `mono 11`. | +| Fallbacks | No `signingFingerprint` on `InstalledApp` → render `Open` state with caption "Unsigned" (`T.ink2`). | + +### 1.5 VersionDelta + +| Field | Value | +|---|---| +| Answers | "How risky is this update?" (`patch | minor | major`) | +| Inputs | `delta: SemverDelta = Patch | Minor | Major | Unknown`, `tint: Color? = null` | +| Geometry | `Patch` = one dot (4dp). `Minor` = two dots (4dp, 3dp gap). `Major` = filled bar 14×4 with a 1dp slash at 60° through it. | +| Colors | tint defaults to `T.primary` for patch/minor; `T.danger` for major. Unknown → grey dot at `T.ink2`. | +| Behavioral rules | Computed from `installedVersion` vs `latestVersion` via `VersionMath` (exists). When parse fails → `Unknown`. | +| Cross-platform | Pure Canvas. | + +### 1.6 VersionStack + +| Field | Value | +|---|---| +| Answers | "How far behind am I?" | +| Inputs | `skippedCount: Int`, `maxBars: Int = 6`, `barHeightStep: Dp = 1.5.dp`, `accent: Color? = null` | +| Geometry | A row of N vertical bars, each `width 2.5dp`, growing in height by `barHeightStep` per bar (4dp, 5.5dp, 7dp, …). Bars beyond `maxBars` collapse into a single bar with `+` suffix. | +| Colors | Bars use `accent.c` if provided, else `T.primary`. | +| Use sites | Apps tab badge (Android bottom nav, DESIGN.md §9.1), update banner inline glyph (DESIGN.md §11.2 / patterns.md §"Update banner"), Library row trailing indicator. | +| Fallbacks | `skippedCount == 0` → composable returns nothing (no empty render). When the data layer doesn't expose "skipped" history, count = `if (isUpdateAvailable) 1 else 0`. | + +### 1.7 PermDot + +| Field | Value | +|---|---| +| Answers | "How dangerous are the permissions?" | +| Inputs | `risk: PermRisk = Low | Moderate | High`, `size: Dp = 10.dp`, `withHalo: Boolean = false` | +| Geometry | Single dot. Halo = 1px ring `1.5dp` outside the dot at 35% alpha. | +| Colors | `tokens.json#status.perm.{low,moderate,high}`: `#6BA068 / #C49652 / #B83A2C`. | +| Use sites | APK Inspect screen permission groups (Android only); vital signs grid "PERMISSIONS" tile on Detail. | +| Fallback | Not an APK or no permission breakdown → tile shows "—" instead of a dot. | + +### 1.8 PlatformGlyph + +| Field | Value | +|---|---| +| Answers | "Will it run on my OS?" | +| Inputs | `platform: DiscoveryPlatform`, `supported: Boolean`, `size: Dp = 18.dp` | +| Geometry | Monochrome silhouettes (DESIGN.md §4.1) — phone, window, apple, penguin. Filled when `supported`, `1.2dp` dashed stroke when not. | +| Source enum | `core/domain/.../DiscoveryPlatform` (`Android, Macos, Windows, Linux`). | +| Colors | Always `LocalContentColor` (defaults to `T.ink`). Never carries the per-app accent. | + +### 1.9 TopicGlyph + +| Field | Value | +|---|---| +| Answers | "What kind of app is this?" | +| Inputs | `topic: String`, `size: Dp = 14.dp` | +| Geometry | Micro-pictograms from `tokens.json#topicGlyphs.supported`: `self-hosted, mobile, photo, video, book, manga, key, audio, backup, reader, cross-platform, cloud`. | +| Aliasing | `tokens.json#topicGlyphs.topicAliases` handles `password-manager → key`, etc. If neither match → return null Composable (do not render — DESIGN.md §4.2 "or omit"). | +| Behavioral rules | At most **3 per card** (DESIGN.md §4.2). Always monochrome `T.ink2`. Never colored. | + +### 1.10 SignalBars + +| Field | Value | +|---|---| +| Answers | "How fast is this mirror?" | +| Inputs | `tier: Int (0..4)`, `size: Dp = 14.dp` | +| Geometry | 4 ascending bars (3dp wide, gap 1.5dp, heights 4/7/10/13dp). Filled = `T.primary`. Unfilled = `outline @ 0.5`. | +| Use sites | `MirrorPickerScreen` rows; download speed indicator in toast. | +| Fallbacks | Mirror health not measured yet → tier 0 (all unfilled). Add caption "Untested" in `caption` text. | + +### 1.11 DownloadWeight + +| Field | Value | +|---|---| +| Answers | "How widely adopted?" | +| Inputs | `downloads: Long`, `maxSize: Dp = 14.dp`, `tint: Color? = null` | +| Geometry | Filled circle whose radius scales `log10(downloads + 1)` mapped to `[4.dp, maxSize/2]`. Caps at maxSize. | +| Colors | `tint ?: T.ink2`. | +| Behavioral rules | Pair with `mono` count text. Never use alone in dense rows — `StarTier` already does adoption. Reserve for Detail "vital signs" and Developer profile. | +| Fallbacks | `downloads == 0` → render only the caption "—". (Forgejo repos may have 0 because the asset aggregation is best-effort.) | + +### 1.12 LicensePosture + +| Field | Value | +|---|---| +| Answers | "Is this restrictive?" | +| Inputs | `spdxId: String?`, `size: Dp = 16.dp` | +| Geometry | A small tile (square, radius 3dp): filled `©` glyph for copyleft, dashed-border `·` glyph for permissive, no glyph for unknown. | +| Buckets | `tokens.json#licenses.copyleft` and `licenses.permissive`. | +| Colors | Foreground `T.ink2`. Filled bg uses `T.surface2` for copyleft (visual weight), transparent for permissive (visual lightness). | +| Fallbacks | Not in either bucket → return null. | + +### 1.13 CookieShape (Expressive) + +| Field | Value | +|---|---| +| Use sites (3 total) | Brand "G" mark (top-left on every primary surface); user identity tile (Profile); active bottom-nav tab on Android. DESIGN.md §4.3, §15. | +| Path | `tokens.json#shape.cookie.path` (9-petal organic). ViewBox `0 0 100 100`. Implement as a `Shape` derived from `androidx.compose.ui.graphics.Path` so it can be used as `clip` + as the path of a `BorderStroke` (Compose: `GenericShape` constructor returning a `Path` scaled to size). | +| Inputs | `letter: String? = null`, `tint: Color = T.primary`, `size: Dp = 32.dp`, `contentColor: Color = Color.White` | +| States | Default (filled `tint`); pressed (`tint @ 0.85`); disabled (replaced with circle, not Cookie — Cookie is identity-only). | +| Behavioral rules | **Never multiple Cookies adjacent** (DESIGN.md §4.4). Desktop drawer's brand mark + user tile sit at opposite ends of the drawer — that satisfies the rule. | + +### 1.14 Squiggle (Expressive) + +| Field | Value | +|---|---| +| Use sites | Section headings; bottom-sheet headings; confirm-dialog headings; diagnostics-card separators. **One per heading** (DESIGN.md §15). | +| Path | `tokens.json#shape.squiggle.path`. Aspect 40×5, stroke 1.6px. | +| Inputs | `width: Dp = 40.dp`, `color: Color = T.primary`, `opacity: Float = 0.6f` | +| Cross-platform | Pure Canvas. | + +--- + +## 2. Component catalogue + +All components live in `core/presentation/.../components/`. Shapes referenced as `GhsShapes.X` where X is one of `chip, row, cardSm, card, cardLg, hero, heroLg, wonky, wonkyAlt, wonkySearch` — these mirror `tokens.json#shape.radii.*` and `shape.wonkySquircle.*`. + +### 2.1 Buttons + +| Variant | Use | Visual spec | +|---|---|---| +| `GhsButtonPrimary` | Install, Update, Open, Sign in | Wonky squircle `GhsShapes.wonky`. Height `48.dp` (Android touch), `40.dp` desktop. Padding `horizontal 18.dp, vertical 10.dp`. Background `T.primary`. Content color `Color.White`. Elevation: `Modifier.shadow(8.dp, GhsShapes.wonky, ambientColor = T.primary.copy(alpha=0.4f), spotColor = T.primary.copy(alpha=0.6f))`. Press: scale 0.97, 100ms. Disabled: `T.primary @ 0.4`, no shadow. | +| `GhsButtonAccent` | Update inside an app-context card | Same as Primary but `background = accent.c`, shadow uses `accent.c`. Used for the in-card Update CTA (patterns.md §"Update banner"). | +| `GhsButtonTinted` | Get, Read more, See all | `GhsShapes.card`. Background `T.tintP`. Content `T.primary`. No shadow. Height `40.dp` / `36.dp`. | +| `GhsButtonOutline` | Inspect, Refresh, Cancel | `GhsShapes.card` or `RoundedCornerShape(50%)` (pill) for nav-row "Cancel". 1.dp border `T.outline`. Background transparent. Content `T.ink`. | +| `GhsButtonDanger` | Destructive confirm | Wonky. Background `T.danger`. Content white. Same shadow recipe but with `T.danger`. | +| `GhsIconButton` | Back, share, favourite, more, dismiss | 36×36.dp transparent box (Desktop) / 48×48.dp (Android). Centered glyph `20.dp`. Tap ripple `T.ink @ 0.08` (design-system.md §10.1). | + +State matrix for every button: + +| State | Bg delta | Content delta | Border / shadow | +|---|---|---|---| +| default | base | base | as defined | +| hover (desktop) | `+5%` lighten / `−5%` darken | unchanged | shadow `+2.dp` for Primary only | +| pressed | `-8%` lightness | unchanged | shadow `-2.dp`, scale `0.97f` 100ms | +| focused (keyboard) | base | base | 2.dp ring `T.primary @ 0.6`, offset `2.dp` | +| disabled | `α 0.4` | `α 0.6` | no shadow | + +### 2.2 Chips + +| Variant | Visual spec | +|---|---| +| `GhsChipFilter` (on) | `GhsShapes.chip`, padding `H 12.dp V 5.dp`, font `Inter Tight 12 / 600`. Bg `T.tintP`, content `T.primary`, 1.dp border `T.primary @ 0.33`. Trailing `×` 14.dp when removable. | +| `GhsChipFilter` (off) | Same shape. Bg transparent. Content `T.ink`. 1.dp border `T.outline`. | +| `GhsChipAdd` | Dashed 1.dp border `T.outline`. `+ Add filter`. Same height & font as filter chip. | +| `GhsChipPill` | `RoundedCornerShape(50%)`. Used only for the search-suggestion recent-query chips. | + +Touch target: chip rows on Android pad an extra 6.dp vertically (so visual 26.dp height → 38.dp touch row). Hit testing extends to the gap between chips. + +### 2.3 Cards + +| Variant | Visual spec | +|---|---| +| `GhsCardLead` (hero with bloom) | Shape `GhsShapes.wonkyAlt`. Padding `20.dp 22.dp 18.dp`. Background `T.surface` with a `RadialGradient` overlay (center 30% from top-left, radius `card.maxWidth`, color stops `[accent.lt @ 0.6 → Color.Transparent]`). Border `1.dp T.outline @ 0.4`. Soft shadow `0 12.dp 32.dp -12.dp T.ink @ 0.18`. Cap width on Desktop at `LocalContentWidth` from `core/presentation/.../locals/LocalContentWidth.kt`. | +| `GhsCardCompact` | Shape `GhsShapes.card`. Padding `14.dp`. Background `T.surface`. 1.dp border `T.outline`. Soft shadow `0 1.dp 0 ink@0.04, 0 8.dp 22.dp -16.dp T.ink @ 0.18` (design-system.md §8). Internal layout per DESIGN.md §7.3 (avatar+ring | name+secondary; description 2-line clamp; topic glyphs row; dashed divider; StarTier + count, platform silhouettes). | +| `GhsCardListRow` | Shape `GhsShapes.row`. Padding `H 12.dp V 10.dp`. Bg `T.surface`. No border (rely on row separators). | +| `GhsCardInstall` (Detail) | Shape `GhsShapes.heroLg`. Padding `18.dp`. Bg `T.surface`. Holds: Primary CTA full-width, secondary asset selector, wax seal anchored top-right (`offset(x=8.dp, y=-8.dp)` + `rotate(-6f)`). | +| `GhsCardIntegrity` | `T.successT` bg, 1.dp `T.success` border, white-on-success check badge. Used as inline alternative for wax-intact in compact contexts. | +| `GhsCardHeroGradient` (Profile) | `linear-gradient(135deg, T.tintP 0%, T.surface 60%)`. Shape `GhsShapes.heroLg`. (patterns.md §"Hero card with gradient" — `tintP → surface` only.) | + +### 2.4 Section header + +``` +[Glyph 20dp] [Fraunces italic 22/600 -0.02em] [· sub-count caption ink2] [See all ›] + [~~~ Squiggle 36–42dp wide, 1.6dp stroke, primary @ 0.6 ~~~] +``` + +| Spec | Value | +|---|---| +| Height | Title row 28.dp + squiggle 8.dp + bottom pad 12.dp | +| Glyph + title gap | 8.dp | +| Squiggle | Aligned under the title's first 36–42dp | +| `See all ›` | `GhsButtonTinted` size-small, height 28.dp, padding `H 10.dp V 4.dp` | +| Meta variant | If section is meta-label not editorial: drop italic + squiggle, use `H3-meta` style from `tokens.json#typography.scale.h3-meta` (Inter Tight 700, uppercase, tracking 0.06em). | + +### 2.5 Banner + +| Spec | Value | +|---|---| +| Shape | `GhsShapes.card` | +| Bg / border (4 variants) | `info: T.tintP / T.primary@0.33`, `success: T.successT / T.success@0.33`, `warn: accent.lt / accent.c@0.33`, `error: T.dangerT / T.danger@0.33` | +| Padding | `H 12.dp V 10.dp` | +| Layout | `[Glyph 18.dp] [Text Inter Tight 13/500 + optional mono 12 detail] [Action button optional] [× dismiss 18.dp]` | +| Use sites | clipboard banner (Home), update banner (Apps, Detail), integrity warnings (Detail), rate-limit notice (any screen) | + +### 2.6 Vital signs 2×2 grid (Detail) + +| Spec | Value | +|---|---| +| Container | 2 cols × 2 rows, gap `12.dp`, each tile `GhsShapes.cardSm`, bg `T.surface2`, padding `12.dp` | +| Tile order (fixed) | `RELEASED · MAINTAINED · STARS · PERMISSIONS` (DESIGN.md §7.7) | +| Tile internal | Glyph (22.dp height) → value `Fraunces italic 600 / 13.sp` colored to the signal → label `caption uppercase 9.5.sp`. | +| Signal colors | RELEASED uses `status.freshness.*`. MAINTAINED uses heartbeat color. STARS uses `T.ink`. PERMISSIONS uses `status.perm.*`. | +| Empty data | Permissions on a non-APK repo → render "—" centered, no glyph; label stays. | + +### 2.7 Wax-seal trust card + +Detail screen (DESIGN.md §7.8). Spec already covered in §1.4 visual; container is `GhsCardInstall`'s top-right anchor (Desktop) or a separate `GhsCardCompact` with the wax seal as the leading 44.dp glyph (Android). + +### 2.8 Bottom sheet + +Compose's `ModalBottomSheet`. DESIGN.md §16.1. + +| Spec | Value | +|---|---| +| Shape | `RoundedCornerShape(topStart=24.dp, topEnd=18.dp, bottomEnd=0.dp, bottomStart=0.dp)` (wonky top corners only) | +| Bg | `T.surface`. Optional 1.dp top border `T.outline`. | +| Drag handle | 36×4.dp pill, `T.ink2 @ 0.3`, top margin 8.dp | +| Heading | Fraunces italic 20 + Squiggle below. Top pad 12.dp. | +| Action row | Sticky bottom, padding 16.dp. Cancel outline (left), primary wonky (right). LTR. | +| Scrim | `T.ink @ 0.5` | +| Animation | Slide-up 240ms ease-out; scrim 180ms fade | +| Dismiss | tap scrim **unless** the sheet hosts an irreversible action | + +Use sites: asset picker (Details), mirror picker, language picker, library import wizard, asset filter chooser. + +### 2.9 Confirm dialog + +DESIGN.md §16.2 / patterns.md §"Confirmation dialog". Use `BasicAlertDialog`. + +| Spec | Value | +|---|---| +| Shape | Wonky `GhsShapes.wonkyAlt`. Max-width 320.dp mobile / 400.dp desktop. | +| Bg | `T.surface`. Scrim `T.ink @ 0.55`. | +| Optional context glyph | Centered top, 36.dp. Picks from §1: WaxSeal cracked, PermDot red ring, CookieShape, VersionStack. | +| Heading | Fraunces italic 18, centered, weight 600. **Specific question form** ("Uninstall immich?", not "Are you sure?"). | +| Body | Inter Tight 13, `T.ink2`, max 3 lines. Explains *consequence*. | +| Actions | Right-aligned. Cancel (outline) left, Confirm (wonky primary or wonky danger) right. | +| Touch | Min 48.dp button height. | + +### 2.10 Toast + +DESIGN.md §16.3. Use Compose's `SnackbarHost` with a custom Snackbar composable that uses our wonky squircle. + +| Spec | Value | +|---|---| +| Shape | `GhsShapes.wonky` | +| Bg / border | by variant (info/success/error/warn) same matrix as Banner §2.5 | +| Leading glyph | Mandatory. From silent vocabulary (DESIGN.md §16.3). | +| Body | Inter Tight 13/600 + optional mono in `T.ink2` | +| Position Android | Bottom-center, 84.dp from bottom (above gesture-nav). Width = `screen − 32.dp`. | +| Position Desktop | Bottom-right, 24.dp inset, max-width 380.dp. | +| Duration | info 3s, action 4s with "Undo", error 6s, **cracked-seal sticky until tap**. | +| Stack | Max 3 visible. Newer push older up. | + +### 2.11 Full-screen sheet + +DESIGN.md §16.5. Used for OAuth device flow, PAT entry, Library Imports wizard, Web-OAuth handoff "waiting" screen. + +| Spec | Value | +|---|---| +| Layout Android | Full-screen Composable host inside the existing nav graph (`AuthenticationScreen`, `ExternalImportScreen`). | +| Layout Desktop | Modal-style dialog 480×640, wonky `GhsShapes.heroLg`. | +| Top bar | Back arrow (24.dp). No title. | +| Identity mark | 64–96.dp CookieShape `letter = "G"` (sign-in) or topic glyph (imports). | +| Heading | Fraunces italic 24 + Squiggle. | +| Numbered steps | Primary-tinted circle markers (`size 18.dp`, bg `T.tintP`, text `T.primary`). Step text Inter Tight 14/500. | +| Code reveal box | Mono 28–32, wonky border 1.5.dp `T.primary`, padding 16.dp. Tap → copy → toast. | +| Polling indicator | Heartbeat glyph (re-using §1.2) + caption "waiting" | +| Fallback CTA | Always offer PAT path as `GhsButtonOutline` at bottom. | + +### 2.12 Dropdown menus + +Used by: translation language picker (already in `details/components/LanguagePicker.kt`), sort order menus, palette picker in Tweaks, "more" `⋯` actions. + +| Spec | Value | +|---|---| +| Shape | `GhsShapes.card` | +| Bg | `T.surface`, 1.dp border `T.outline`, shadow `0 10.dp 24.dp -12.dp T.ink @ 0.35` | +| Item | Height 40.dp. Padding `H 12.dp`. Optional leading glyph 16.dp, label Inter Tight 13, optional trailing mono detail or checkmark `T.primary`. | +| Hover (desktop) | bg `T.tintP @ 0.5` | +| Pressed | bg `T.tintP` | +| Selected | trailing `✓ T.primary` | + +### 2.13 Bottom nav (Android only) + +DESIGN.md §9.1 + MIGRATION.md (4 → 3 tabs + detached search). + +| Spec | Value | +|---|---| +| Tabs (final order) | `Home`, `Search`, `Apps` (a.k.a. Library), `Profile` | +| Height | 64.dp + system gesture inset | +| Bg | `T.surface` with shadow `0 8.dp 24.dp -12.dp T.ink @ 0.32` | +| Inactive | Outline glyph 20.dp `T.ink2`, label Inter Tight 11/500 `T.ink2` below | +| Active | CookieShape (size 40.dp) bg `T.primary` behind a knocked-out white 20.dp glyph; label Fraunces italic 12/600 `T.primary` below | +| Apps tab badge | VersionStack at top-right (replaces M3 numeric badge) when `pendingUpdates > 0` | +| Tap state | Tap layer `T.ink @ 0.08`, 120ms | + +(Desktop has no bottom nav — drawer §3 below.) + +### 2.14 Desktop drawer (Desktop only) + +Replaces the current "side rail / drawer hybrid" with the spec from DESIGN.md §8.1. + +| Spec | Value | +|---|---| +| Width | 240.dp | +| Bg | `T.bg` (sits flush against window chrome) | +| Brand row | CookieShape (28.dp, "G", `T.primary`) + "GitHub Store" Inter Tight 14/600 | +| Search input | Wonky `GhsShapes.wonkySearch`, 1.dp `T.outline`, leading glyph, trailing `⌘K` mono caption. Tap → `SearchScreen`. | +| Nav item | Height 40.dp. Padding H 12.dp. Glyph 20.dp + label Inter Tight 14. Active: bg `T.tintP`, content `T.primary`, weight 600, shape `radD(13,10)` (`GhsShapes.row`). Inactive: transparent, content `T.ink`, weight 500. | +| User card (bottom) | CookieShape with user initial; primary fill. Subtitle in caption ink2 ("3 updates"). | +| Sticky bottom group | `Settings ⌘,`, `Shortcuts ?`. | + +--- + +## 3. Screen-by-screen visual spec + +The screens below are scoped to the existing modules. Files cited are `commonMain` unless noted. The "Data fields" column references actual domain models — `GithubRepoSummary`, `GithubRelease`, `InstalledApp`, `GithubUser`, `GithubUserProfile`, `Announcement`, `HostToken`, `MirrorConfig`, `ApkInspection`. + +### 3.1 `feature/home/` — Discovery feed + +Files: `HomeRoot.kt`, `HomeViewModel.kt`, `HomeState.kt`, `components/HomeFilterChips.kt`. + +**Layout (vertical scroll, Android + Desktop):** + +``` +[Brand row Android: G-cookie · "GitHub Store" · User-cookie] (Desktop: drawer handles brand) +[Search input (wonky) — tap → SearchScreen] +[Clipboard banner — if clipboard URL parses to a repo, conditional] +[Time-window chip row: Today · Week · Month · All] (HomeFilterChips, keep) +[Lead release card · GhsCardLead with accent radial bloom] +[Section: Hot releases squiggle] +[ → Horizontal scroll Android / 2-col grid Desktop of GhsCardCompact ] +[Section: Trending now squiggle] +[ → Vertical list with #N rank chips on the left ] +[Section: Most popular squiggle] +[ → Vertical list, rank in Fraunces italic 0.55 opacity ] +[Section: From your stars (auth required) squiggle ] +[ → Vertical list, APK-shipping starred repos] +``` + +| Component | Data | +|---|---| +| Lead release | `GithubRepoSummary` + freshest `GithubRelease.publishedAt`. Accent from §6. | +| Hot releases | `GithubRepoSummary[]` filtered by time window. | +| Trending now | Backend rank → `position #N`. If backend missing (DESIGN.md §11), use local sort proxy and drop the rank chip (don't fake). | +| Most popular | Sorted by `stargazersCount`. | +| From your stars | `StarredRepository`, scope: APK-shipping. Empty state if signed-out. | + +Android vs Desktop deltas: +- Android: top bar (52.dp) with G-cookie left, U-cookie right. +- Desktop: drawer is persistent; right pane shows the same vertical list at `LocalContentWidth` (COMPACT 720 / WIDE 960 / EXTRA_WIDE 1200). The "Most popular" and "Trending" lists become 2-column grids when content width ≥ WIDE. + +### 3.2 `feature/search/` — Search + +Files: `SearchRoot.kt`, `SearchViewModel.kt`, `SearchState.kt`. + +``` +[Top bar: ← back · search input (wonky, autofocus) · ⋯ filters] +[Source toggle: GitHub | Codeberg | Custom forge] (NEW chip row) +[Filter chips: Platform (Android/Win/macOS/Linux/All) · Sort · Language] +[Recent queries (when empty) — chip cloud] +[Results list — GhsCardListRow] +``` + +| Component | Data | +|---|---| +| Source toggle | `RepositorySource` enum already exists. GitHub default. Codeberg defaults to `codeberg.org`. Custom forge opens dropdown picker of user-added hosts. | +| Result row | Avatar+FreshnessRing left, name (Fraunces italic) + owner caption, StarTier+count, PlatformGlyphs trailing. Tap → `DetailsScreen(sourceHost=…)`. | + +Android vs Desktop: +- Android: input is the only thing in the top bar. Filters via a `⋯` menu opening a bottom sheet. +- Desktop: filters expand inline as a chips row under the input. Source toggle is a segmented control on the right. + +### 3.3 `feature/details/` — Repo detail + +Files: `DetailsRoot.kt`, `DetailsViewModel.kt`, `components/sections/{Header,About,Stats,WhatsNew,Owner,ReleaseChannel,ReportIssue,Logs}.kt`. + +**Android layout (centered hero, stacked):** + +``` +[Top bar: ← ↗ open external · ♡ favourite · ⋯] +[Hero: 92.dp avatar in FreshnessRing + repo name (Fraunces italic 28) + owner · TopicGlyphs (max 3) + StarTier ★★★★★ + count · DownloadWeight mono] +[Install panel (GhsCardInstall): + [Primary wonky CTA full-width — Install · 48 MB] + meta row: PermDot · PlatformGlyph(arch) · SignalBars (mirror) + wax seal card (top of section, full width on Android)] +[Vital signs 2×2: RELEASED · MAINTAINED · STARS · PERMISSIONS] +[About section preview (3 lines clamp) → "Read more"] +[What's new preview → "Show all versions"] +``` + +**Desktop layout (two-column):** + +``` +[Top bar: ← (when entered from Home/Search) breadcrumb · share] +[Hero block left-aligned 65% width] [Right column 35% — wax seal card rotated -6°, +[Install panel — same as Android] vital signs 2×2, platform silhouettes] +[About + What's new tabs underneath] +``` + +| Component | Data | +|---|---| +| Hero | `GithubRepoSummary` + chosen `GithubRelease`. Accent from §6. | +| Install panel | Asset picker bottom-sheet, primary CTA varies per `SmartInstallButton` (already exists). Wax seal uses `InstalledApp.signingFingerprint` and an expected fingerprint (when previously installed) — currently the data layer doesn't store expected fingerprint for non-installed repos; **fallback: Open state with "Signed by maintainer" caption.** | +| Vital signs | RELEASED uses `releaseRecency` days; MAINTAINED uses `updatedAt` delta; STARS uses `stargazersCount`; PERMISSIONS uses `ApkInspection` if available (Android only), else "—". | +| About preview | First 3 lines of README. "Read more" opens inner About screen (next sub-section). | +| What's new preview | Latest `GithubRelease.description` first 3 lines + `JetBrains Mono` tag. | + +**Detail inner — About / What's new (DESIGN.md §8.4):** + +``` +[Top bar: ← repo-name · [About | What's new] · 🌐 EN▾ (TranslationControls existing)] +[About: rendered README, multiplatform-markdown-renderer] + OR +[What's new: split view] + [Version rail (left)] v2.7.5 · 2d ago ← current + v2.7.4 · 1w ago + v2.7.3 · 2w ago + v2.7.0 [YOU] ← installed badge + [Selected version notes (right)] Fraunces italic title, Inter Tight body, mono version tag +``` + +Version rail on Android collapses to a vertical list above the notes (single column). Tap a row → notes update inline. + +### 3.4 `feature/apps/` — Library (installed apps) + +Files: `AppsRoot.kt`, `AppsViewModel.kt`. Also covers `ExternalImportScreen`, `StarredPickerScreen` (both lives in same module). + +**Android layout (single pane):** + +``` +[Top bar: Library Fraunces 28 · "5 apps · 1 update available" [filter ⚙]] +[Update banner if any: VersionStack · vOLD → vNEW · primary wonky Update CTA — patterns.md §"Update banner"] +[Segmented chip row: Installed · Updates [1] · Pending] +[List of GhsCardListRow per installed app] + [Avatar+FreshnessRing] [name (Fraunces italic) · mono version · heartbeat] [trailing: Open / Update / Inspect] +``` + +**Desktop layout (TWO-PANE — NEW for GHS):** + +``` +[Drawer 240] [List pane 380] [Detail pane 660] + [Header: "Library · 5 apps · 1 upd"] + [Filter tabs: Installed/Updates/Stars/Recent] + [Update banner inline (sticky top)] + [Rows of installed apps] + [Hero (avatar+ring, name, meta)] + [Update/Open CTA] + [Install panel] + [About preview "Read more" → inner] + [Right column: wax seal, vitals] +``` + +The two-pane is **new** — propose adding a `LibraryDetailHost` composable that hosts either the empty state ("Select an app") or the Detail screen (reuse the existing `DetailsRoot` with a `embedded=true` prop that hides its own top bar). Selection state lives in `AppsViewModel` so deep links from Home/Search still route through the existing `DetailsScreen` graph entry. + +| Data fields | Source | +|---|---| +| Row avatar+ring | `InstalledApp.repoOwnerAvatarUrl` + `latestReleasePublishedAt` (days delta) | +| Heartbeat | `lastUpdatedAt` for freshness, gated by row density — only show heartbeat when ring is suppressed (e.g. updates-only filter mode) | +| Trailing CTA | `hasActualUpdate()` extension already in `InstalledApp.kt` | +| Update banner | `pendingUpdates = installedApps.count { it.hasActualUpdate() }` | +| Inspect button | Android only (`isAndroid()`) | + +ExternalImportScreen and StarredPickerScreen reuse `GhsCardListRow` with a leading checkbox (icon shell shape `GhsShapes.cardSm`, 24.dp, bg `T.tintP`, primary check). + +### 3.5 `feature/profile/` — User profile + +Files: `ProfileRoot.kt`, `ProfileViewModel.kt`, `SponsorScreen.kt` (kept as Sponsor route but **rebranded** — see don't-build list). + +``` +[Top bar: Profile Fraunces 28 · settings ⚙ → TweaksScreen] +[Identity card (GhsCardHeroGradient — patterns.md §"Hero card with gradient")] + [Avatar in Cookie shape 64.dp · primary fill] + [@username Fraunces italic 22] [Inter Tight 13 ink2 bio] + [stats row: followers · following · public repos] ← from GithubUserProfile +[Section: Activity (squiggle)] + [Recently viewed → RecentlyViewedScreen] + [Favourites → FavouritesScreen] + [Starred (auth) → StarredReposScreen] +[Section: Connect (squiggle, MIGRATION.md "no donations")] + [6-cell grid: GitHub · Mastodon · Bluesky · Discord · email · website] + [Business inquiries row → opens default mail client] +[Section: App (squiggle)] + [Tweaks → settings] [What's new history → WhatsNewHistoryScreen] +[Sign out → confirm dialog] +``` + +Android & Desktop: identical layout. Desktop respects `LocalContentWidth`. + +### 3.6 `feature/dev-profile/` — Developer profile of an owner + +Files: `DeveloperProfileRoot.kt`, `DeveloperProfileViewModel.kt`. + +``` +[Top bar: ← @owner] +[Identity card: avatar (NO cookie — owner is not "the user") + name + bio + follower count] +[Section: Repositories that ship binaries (squiggle)] + [List of GhsCardListRow per repo] +[Section: Their stats (squiggle)] + [DownloadWeight summed · total stars StarTier · repo count] +``` + +Data: `GithubUserProfile`. + +### 3.7 `feature/tweaks/` — Settings + +Files: `TweaksRoot.kt`, `TweaksViewModel.kt` (note: 57KB — large state), `components/{ToggleSettingCard, SectionText, CustomForgesDialog, ClearDownloadsDialog}.kt`, plus subfolders `hosttokens/`, `mirror/`, `feedback/`, `hidden/`, `skipped/`. + +Section order (top to bottom): + +1. **Appearance** (squiggle): + - Two-axis theme picker (§7 below) — palette row + mode segmented control + - Content width: `COMPACT / WIDE / EXTRA_WIDE` (existing) — segmented control on Desktop, dropdown on Android + - Font theme (keep existing `FontTheme.CUSTOM/SYSTEM`) +2. **Sources** (squiggle): + - Custom forges (`CustomForgesDialog` — keep) — list of `{host, label}` with Add/Edit/Delete + - Per-host tokens → `HostTokensScreen` (existing route) + - Mirror picker → `MirrorPickerScreen` +3. **Translations** (squiggle): + - Provider segmented: Google · Youdao · LibreTranslate · DeepL · Microsoft (`TranslationProvider` enum existing) + - Per-provider config card (sub-fields like API key, mirror URL) — gated by selection +4. **Library** (squiggle): + - Installer preference (Android only) — System / Shizuku / Root + - Skipped updates → `SkippedUpdatesScreen` + - Hidden repositories → `HiddenRepositoriesScreen` +5. **Privacy** (squiggle): + - Telemetry toggle + - Proxy config +6. **About** (squiggle): + - Version (mono) `1.8.3 (18)` + - Announcements (existing) — route to `AnnouncementsScreen` + - What's new history → `WhatsNewHistoryScreen` + - Send feedback → diagnostics card (DESIGN.md §16.4) with "Email" + "GitHub issue" dual CTA + +Each setting row uses `SetRow` (patterns.md §"Form / Settings group"): 36.dp tinted icon shell (bg `T.tintP`, content `T.primary`, shape `GhsShapes.cardSm`) + name + trailing toggle/value/chevron. + +### 3.8 `feature/favourites/`, `feature/starred/`, `feature/recently-viewed/` + +Presentation-only modules. All three: + +``` +[Top bar: ← Section title Fraunces 28 · count caption] +[Sort/filter chips (optional)] +[List of GhsCardListRow] +[Empty state: Squiggle illustration absent (DESIGN.md §"empty"), Fraunces italic headline, body, primary outline CTA "Browse home" / "Sign in" / "Search apps"] +``` + +Differences: +- **Favourites**: data from `FavouritesRepository.favouriteRepos`. Tap → DetailsScreen. +- **Starred**: data from `StarredRepository`, gated by auth. Empty state changes when signed-out → CTA "Sign in" → AuthenticationScreen. +- **Recently viewed**: data from `SeenReposRepository`. Sort by `lastSeenAt` desc. Swipe-to-clear on Android; clear-all button on Desktop. + +### 3.9 `feature/auth/` — Sign in flow + +Files: `AuthenticationRoot.kt`, `AuthenticationViewModel.kt`. + +Renders as **Full-screen sheet** (§2.11). Three states: + +| State | Visuals | +|---|---| +| Web OAuth handoff (default) | CookieShape "G" 80.dp; heading "Sign in with GitHub"; primary wonky CTA "Open in browser"; small caption "We'll redirect you back via githubstore://auth". Below: outline "Use device code instead" + outline "Use Personal Access Token". | +| Device flow (fallback) | DESIGN.md §16.5 layout: numbered steps, code reveal (mono 28), heartbeat polling indicator + "Polling github.com…" caption. Cancel button (back arrow). | +| PAT entry (last resort) | Single text field (mono), info banner about scopes, primary "Verify token" CTA. | + +`AuthPath` (`Backend`|`Direct`) state already in `SavedStateHandle`; the UI just renders different bodies for the same shell. + +--- + +## 4. Build order (phases) + +Each phase ends with a runnable, testable artifact. Branch `feat/design-system-refresh` should accumulate atomic commits per the user's PR/commit sizing memory. + +### Phase 0 — Tokens + theme + fonts + +**Scope:** +- Add `core/presentation/.../theme/tokens/` directory: + - `GhsTokens.kt` (data class with `bg`, `surface`, …) + - `GhsStatus.kt` (palette-independent — wax, freshness, perm, trend) + - `GhsPalette.kt` (enum NORD/CREAM/FOREST/PLUM) + `GhsPalettes` provider mapping each palette × {light,dark} from `tokens.json` + - `GhsShapes.kt` (composable shapes: `chip, row, cardSm, card, cardLg, hero, heroLg, wonky, wonkyAlt, wonkySearch`) + - `GhsTypography.kt` (Material `Typography` populated from `tokens.json#typography.scale`) +- Add fonts: Fraunces (italic 600), Inter Tight (400–700), JetBrains Mono (500–700). Use `composeApp/.../res/font/`. Configure variable-font axis when possible. +- Add Noto fallback families for CJK/Devanagari/Arabic/Hebrew (design-system.md §5.4) via `FontFamily.Default` extensions. +- Add `GhsTheme(palette, isDark, fontTheme, isAmoled, content)` composable that wraps `MaterialExpressiveTheme` and exposes Locals. +- Migrate `TweaksRepository` palette key — replace the existing `AppTheme.{DYNAMIC,OCEAN,…}` with `GhsPalette.{NORD,CREAM,FOREST,PLUM}` (+ keep DYNAMIC under an "if Android & supported" branch — see don't-build §8). +- Tweaks → Appearance: render the two-axis picker (§7). + +**Success criteria:** +- App launches in all 8 combinations (4 palettes × {light, dark}). +- Palette switch in Tweaks updates every screen live within 250ms (medium motion token). +- No hardcoded hex outside `Color.kt` / `GhsPalettes.kt`. +- Existing screens still render (visually unchanged or roughly compatible — they read tokens through compat shims). + +### Phase 1 — Silent vocabulary primitives + preview screen + +**Scope:** all 12 silent primitives (§1.1–§1.12) + a dev-only preview screen behind a Tweaks → "Developer → Show primitives gallery" toggle. Screen renders every primitive in every relevant state (default, hover/pressed where applicable, disabled). + +**Success criteria:** +- Each primitive composable has a `@Preview` (commonMain previews supported in IDE). +- Heartbeat animations honor the `GhsMotionPreference` Local. +- All primitives render correctly under all 4 palettes × {light, dark}. + +### Phase 2 — Expressive primitives + base components + +**Scope:** +- `CookieShape` (§1.13), `Squiggle` (§1.14) +- All buttons (§2.1) +- All chips (§2.2) +- All cards (§2.3) — including `GhsCardLead` with radial-bloom modifier +- Section header (§2.4) +- Banner (§2.5) +- Vital signs grid (§2.6) +- Wax-seal trust card (§2.7) +- Bottom sheet, confirm dialog, toast, dropdown (§§2.8–2.10, 2.12) +- Bottom nav (Android) + Drawer (Desktop) shells, not yet wired to features + +Add to the primitives preview screen: a "Components" tab. + +**Success criteria:** +- Component tab in preview shows every component in every state. +- Cookie shape passes path equality vs `tokens.json#shape.cookie.path` (write a unit test that compares the constructed `Path` against the expected SVG path command stream). + +### Phase 3 — Home migration + +**Scope:** Migrate `feature/home/` to new spec (§3.1). Reuse existing `HomeViewModel`, swap composables. Keep existing data model. + +Visual deltas to verify: +- Lead release card uses `GhsCardLead` with the repo's accent radial bloom. +- Hot releases: horizontal scroll on Android; 2-col grid on Desktop when content width ≥ WIDE. +- Section headers use Squiggle. +- Clipboard banner uses `GhsBanner info` variant. + +**Success criteria:** Home renders correctly in all palette × mode combos; lead-card accent travels from the API-supplied accent or topic-derived fallback (§6). + +### Phase 4 — Apps (Library) migration + bottom nav switch + +**Scope:** Migrate `feature/apps/` to new spec (§3.4). Switch Android bottom nav from current set to `[Home, Search, Apps, Profile]` (4 tabs — see assumption Q1 below; if the spec mandates 3 tabs we'll drop Search to a detached FAB later). Implement two-pane Desktop Library. + +**Success criteria:** +- Library shows Update banner with VersionStack glyph when `pendingUpdates > 0`. +- Bottom nav active tab uses CookieShape behind the glyph. +- Desktop two-pane: selecting a row replaces the detail pane in <250ms; deep-link to a repo via `DetailsScreen` still works. + +### Phase 5 — Details migration + +**Scope:** Migrate `feature/details/` (§3.3). Re-skin `Header`, `About`, `Stats`, `WhatsNew`, `ReleaseChannel`, `Owner`, `ReportIssue`. Replace `StatItem` with vital signs 2×2. + +Anchor the wax seal to the install panel; verify rotated -6° on Desktop only. + +**Success criteria:** +- Detail screen renders all per-app accent surfaces (lead hero bloom, freshness ring outer, install panel bloom). +- Wax seal Cracked state forces card border to `T.danger` and surfaces a sticky toast. + +### Phase 6 — Search migration + +**Scope:** Migrate `feature/search/` (§3.2). Add source toggle (GitHub / Codeberg / Custom forge), preserve existing `SearchViewModel` logic, swap composables for chip/row/banner. + +**Success criteria:** Source toggle drives the existing `sourceHost` plumbing through `ForgejoClientRegistry`. Recent queries chip cloud appears when input is empty. + +### Phase 7 — Auth migration + +**Scope:** Migrate `feature/auth/` to full-screen sheet (§3.9). Re-render web-OAuth handoff, device flow, PAT entry as three bodies of the same shell. Reuse `AuthPath` state. + +**Success criteria:** All three auth paths render with CookieShape identity, Squiggle heading, Heartbeat polling indicator. Tap-to-copy device code emits "Code copied" toast. + +### Phase 8 — Tweaks migration + +**Scope:** Migrate `feature/tweaks/` (§3.7). Build the two-axis theme picker (§7). Wire all settings rows to `ToggleSettingCard` (rename to `GhsSetRow`). + +**Success criteria:** Every section uses Squiggle headers. Two-axis picker swatches show live preview when hovered (Desktop). + +### Phase 9 — Profile + DevProfile migration + +**Scope:** Migrate `feature/profile/` (§3.5) and `feature/dev-profile/` (§3.6). Implement Connect grid + Business inquiries row. **Remove** SponsorScreen donations content; repurpose route to a "Support the project" page that links out to the GitHub Sponsors page externally (browser intent), nothing inline. + +**Success criteria:** Profile renders identity card with `tintP → surface` gradient (only allowed gradient besides Lead bloom). + +### Phase 10 — Favourites / Starred / Recently-viewed migration + +**Scope:** Migrate the three list-only modules to `GhsCardListRow` + empty states. + +**Success criteria:** Empty states have no stock illustration (DESIGN.md §"empty"), use Squiggle + Fraunces italic headline + outline CTA. + +### Phase 11 — APK Inspect (Android only) + +**Scope:** Build the APK Inspect screen (DESIGN.md §9.6). Lives under `feature/apps/presentation/` (Android-only file using `expect/actual` or `androidMain` source set). Uses `ApkInspector` already in `core/domain/`. + +**Success criteria:** Shows wax seal, min/target/compile SDK tiles, activity/service/receiver counts, permissions grouped by `PermRisk`. Permission chips are color-coded inline. + +--- + +## 5. Data honesty audit hooks + +Per primitive — what falls back if the backend doesn't supply the input: + +| Primitive | Required field | Backend nullable? | Fallback | +|---|---|---|---| +| FreshnessRing | `GithubRelease.publishedAt` → days | Yes when no releases ever | Render avatar without ring; caption "No releases" | +| Heartbeat | `GithubRepoSummary.updatedAt` → days | No (always provided) | n/a | +| StarTier | `stargazersCount` | No | n/a | +| WaxSeal | `InstalledApp.signingFingerprint` + expected | Both nullable | If installed but expected missing → `Intact` w/ caption "Signed by maintainer" (truthful: we know it's signed because Android won't install unsigned). If not installed → `Open` w/ caption "Unsigned by us yet". | +| VersionDelta | `installedVersion` + `latestVersion` | Sometimes one null | `Unknown` grey dot | +| VersionStack | history of skipped tags | Not tracked today | `skippedCount = if (isUpdateAvailable) 1 else 0` | +| PermDot | `ApkInspection.permissions` | Non-APK / no inspection | Render "—" in vital signs tile, no glyph in card | +| PlatformGlyph | `availablePlatforms` | Empty list legal | Hide the row entirely (don't show all-dashed) | +| TopicGlyph | `topics` | Often empty on Codeberg | Drop the row (do not invent topics) | +| SignalBars | mirror health | Untested mirror | tier 0 + caption "Untested" | +| DownloadWeight | aggregated `downloadCount` | Forgejo sums may be 0 | Render "—" in the cap text, skip the dot | +| LicensePosture | SPDX id | Often missing | Render nothing | +| CookieShape | identity letter | n/a | Use `?` glyph as fallback (signed-out user tile) | +| Squiggle | n/a | n/a | always renders | + +**Trending and Most-popular sections** (DESIGN.md §11): when the backend's `trendingScore` / `popularityScore` is null, the rank chip `#N` is suppressed — section still renders, the order is the backend list's natural order, but **no pretending**. + +**Per-release "HOT · Nd ago" pill on the Lead card:** derived from `releaseRecency` — when `recency > 30d`, downgrade to "FRESH · Nw ago" / "WARM" per `tokens.json#thresholds.freshness`. Never label something HOT it isn't. + +--- + +## 6. Per-app accent algorithm + +Each repo carries `accent = { c, lt, dt }` (DESIGN.md §2.4). Resolution order (top wins): + +1. **Backend-supplied** — when API returns an `accent` object (not present today; propose adding via `GithubRepoSummary.accentHex: String?` field, server-side derived from avatar dominant color). +2. **Topic-derived** — first matching topic in repo's `topics` against the table below. +3. **Language-derived** — `primaryLanguage` against the language table. +4. **Blue fallback** — `#5E81AC` (Nord primary). + +### 6.1 Topic → accent + +| Topic | `c` | Use case | +|---|---|---| +| `photo`, `photos`, `gallery` | `#5E81AC` | Cool blue (immich-like) | +| `manga`, `comic`, `reader` | `#7E6BA8` | Plum | +| `password-manager`, `security`, `vault` | `#4C6E96` | Navy | +| `podcast`, `audio`, `music` | `#6B8E5A` | Sage | +| `book`, `ebook`, `koreader` | `#9B6B3C` | Amber | +| `messaging`, `chat`, `signal` | `#A35365` | Muted rose | +| `vpn`, `network`, `proxy` | `#5C7A8E` | Slate-blue | +| `note`, `notes`, `markdown` | `#7A6549` | Cream-ink | +| `backup`, `sync` | `#5A6A57` | Forest-ink | +| `self-hosted`, `home-server` | `#356859` | Forest-deep | +| `video`, `media` | `#B8542C` | Cream-primary | + +### 6.2 Language → accent + +| Language | `c` | +|---|---| +| `Kotlin` | `#7E6BA8` | +| `Java` | `#B8542C` | +| `TypeScript`, `JavaScript` | `#5E81AC` | +| `Python` | `#356859` | +| `Rust` | `#A35346` | +| `Go` | `#5C7A8E` | +| `C`, `C++` | `#7A6549` | +| `Swift` | `#B8542C` | +| `Dart` | `#5E81AC` | +| `Ruby` | `#B83A2C` | +| `Shell`, `Bash` | `#6B8E5A` | +| anything else | fallback blue | + +### 6.3 Tint derivation + +``` +lt = mix(c, white, 0.78) // light-mode soft bg fill +dt = c.copy(alpha = 0.22) // dark-mode bg fill +``` + +Stored once per repo (cache in `SeenReposRepository` or a new in-memory map keyed by `repoId`). + +### 6.4 Where the accent appears + +| Surface | Accent role | +|---|---| +| Home Lead card | Radial bloom (`lt @ 0.6`) center-top | +| Hot release compact card | Faint top stripe (4.dp) `lt @ 0.4` | +| Trending / Popular rows | None (rank is the answer) | +| Detail hero | Avatar ring outer arc tint | +| Detail install panel | Primary CTA stays `T.primary`; the small "Update" banner uses `accent.c` | +| Library row | None (apps share visual weight; per-app tint would compete with VersionStack) | +| Favourites / Starred / Recently-viewed rows | None | +| Update banner inside Apps detail pane | Banner bg `accent.lt`, CTA bg `accent.c` (patterns.md §"Update banner") | + +**Never**: bottom nav (accent doesn't follow user, palette does), toast (status colors do the work), confirm dialog, drawer. + +--- + +## 7. Two-axis theme picker (Tweaks → Appearance) + +The picker has two independent axes: + +``` +APPEARANCE +~~ squiggle ~~ + +Palette +[Nord] [Cream] [Forest] [Plum] ← 4 swatches, each 72.dp square, GhsShapes.cardSm + ◉ active (1.5.dp T.primary border + corner check) + +Mode +[ ☀ Light ◐ System 🌙 Dark ] ← segmented control, RoundedCornerShape(50%) outer, each segment GhsShapes.chip +``` + +| Spec | Value | +|---|---| +| Palette swatch | 72×72.dp `GhsShapes.cardSm`. Internally split: top half = light-mode preview (mini bg+surface+primary blocks), bottom half = dark-mode preview. Label below in caption: "Nord" / "Cream" / etc. | +| Active swatch | 1.5.dp `T.primary` border + small `✓` in top-right `T.primary` | +| Mode segmented | Three icon+label segments. Selected segment has `T.tintP` bg + `T.primary` content; others transparent + `T.ink2`. | +| Live preview | On hover (Desktop) the whole app re-themes for 1.5s; on tap, commits. Android: tap commits immediately. | +| Persistence | Two keys in `TweaksRepository`: `palette: GhsPalette`, `themeMode: AppearanceMode = Light | Dark | System`. | +| Migration | The existing `AppTheme` enum (`DYNAMIC, OCEAN, PURPLE, …`) maps once on first launch post-update: OCEAN/SLATE → NORD; PURPLE/AMBER → PLUM/CREAM (closest hue); FOREST → FOREST; DYNAMIC → user explicitly opted into Material You — see don't-build §8. | + +Mobile layout: palette swatches in a 2×2 grid (each 72.dp), mode segmented full-width below. +Desktop layout: palette swatches in a horizontal row of 4, mode segmented to the right. + +--- + +## 8. Don't-build list + +Things explicitly NOT to bring forward, with rationale: + +1. **Donations / Sponsor inline UI** (MIGRATION.md §risky areas). Profile gets Connect + Business inquiries. Keep the `SponsorScreen` route but repurpose: it now just opens GitHub Sponsors externally via `BrowserHelper`. No inline donation cards, no rewards, no tier list. +2. **Material You / Dynamic color override** (themes.md, design-system.md §3). Dynamic color is removed. The current `AppTheme.DYNAMIC` is gone — users who had it get migrated to `NORD` + their existing mode. Reasoning: dynamic colors don't survive the Silent Vocabulary's accent rules — they fight the per-app accent. Add a Tweaks line "We removed dynamic color — pick Nord, Cream, Forest, or Plum instead. The accent now comes from each app's logo." +3. **"Featured" curation** (DESIGN.md §11). The lead release on Home is always "top of filtered Hot list", labeled honestly with `HOT · Nd ago`. Don't invent any "Editor's pick" labels. +4. **Trending percentage chips** ("+15%"). Backend doesn't supply rate of change. Use position `#N` only when backend provides rank; otherwise no rank chip. +5. **Translate-the-app feature** (MIGRATION.md §risky areas). The 13 locale strings stay (`core/presentation` already has them), but no "Translate" UI surface. The Translate provider plumbing in Tweaks is **only** for translating README/release-notes content (existing TranslationControls). +6. **Stock illustrations in empty states**. Use Squiggle + Fraunces italic headline only. RoseFourLoader covers loading. +7. **Card hover-lift animations / scale on hover**. DESIGN.md §6.2 forbids. Use only background tint state-layer at 8% (`T.ink @ 0.08`). +8. **Decorative gradients** anywhere except (a) Lead card accent bloom and (b) Profile identity card `tintP → surface`. No backgrounds gradients on regular cards (DESIGN.md §2.5, design-system.md §2). +9. **Emoji in UI chrome** — replaced by silent primitives or Material Symbols. Allowed only in user-generated content (README, release notes). +10. **Long descriptive sentences** ("Released 37 days ago") when a primitive already says it (the FreshnessRing's fraction is the answer). +11. **Multiple modal dialogs stacked** (DESIGN.md §16.8). Replace with full-screen sheet. + +### 8.1 GHS-specific surfaces the handoff doesn't address (we add them under the new system) + +The handoff was written for a single-source (github.com) app. GHS extends to Codeberg / Forgejo / custom forges + per-host PATs + mirror picker — all foreign to the prototypes. They belong under the new vocabulary as follows: + +| Surface | Where | Visual approach | +|---|---|---| +| **Source toggle (GitHub / Codeberg / Custom)** | Search top, Details top | Segmented control. Each segment is a small platform mark (GitHub octocat → outlined, Codeberg → outlined, Custom forge → dashed plus). | +| **Per-host tokens** | Tweaks → Sources → "Access tokens" | List rows. Each row: host name in mono, label, "Edit / Remove" trailing menu. Add row uses dashed-border chip pattern (§2.2). | +| **Custom forges** | Tweaks → Sources → "Custom forges" | Same row pattern. Adds: live validation chip (SignalBars-style) after entry test. | +| **Mirror picker** | `MirrorPickerScreen` (sub-screen) | Bottom sheet on Android, side sheet on Desktop. Rows: mirror name in mono + SignalBars + last-checked relative time. | +| **Translation provider config** | Tweaks → Translations | Segmented provider picker + per-provider sub-form (mirror URL for LibreTranslate, API key for DeepL, etc.). Existing `KSafe` pattern. | +| **Repo-id-codec foreign host marker** | Anywhere a repo from a non-GitHub source appears | Small platform mark (16.dp) to the right of the owner caption; PlatformGlyph-style outlined silhouette. | + +--- + +## 9. Open questions and assumptions + +10 items, prioritized. Marked **A** (Assumption — I'm picking, flag if wrong) or **B** (Blocker — needs user input before that phase starts). + +1. **[A] Bottom nav tabs = 4 (Home, Search, Apps, Profile).** DESIGN.md §9.1 shows 4. MIGRATION.md §risky says "3-tab + detached search FAB". The handoff is internally inconsistent. I'm picking 4 because (a) Search is heavily used in this app and (b) detached FABs collide with our Shizuku install affordances on Android. Flag if you want 3. +2. **[A] Cookie active indicator on Desktop drawer.** I'm rendering Cookie only on Android bottom-nav active tab; Desktop drawer uses the simpler `T.tintP` background + 13/10 squircle (DESIGN.md §8.1). The rule "Cookie only at 3 touchpoints" survives because Desktop has CookieShape on brand mark + user tile (and no active tab Cookie). +3. **[A] Profile sub-screens (Favourites / Starred / Recently-viewed) keep their own top-level routes.** I'm not collapsing them into a single "Activity" screen. MIGRATION.md §risky implies collapse but the existing nav graph names them separately and the current UI keeps them as distinct routes — easier to migrate one-screen-at-a-time. +4. **[B] Two-pane Library on Desktop — confirm scope.** The handoff §8.3 mandates two-pane. We don't have it today. Phase 4 needs a green light to introduce `LibraryDetailHost` and embed `DetailsRoot` with an `embedded=true` prop (which I have to add). +5. **[B] Repo accent — server-side or client-side derivation?** DESIGN.md §2.4 says "When backend doesn't supply one, derive from avatar dominant color (color-thief)". We don't have a JVM/Android color-thief in `commonMain`. Phase 3 falls back to topic→accent and language→accent (§6) — confirm that's enough until backend ships the field. +6. **[A] WaxSeal "expected fingerprint" storage.** Current `InstalledApp.signingFingerprint` is the *current* one; we don't store an expected baseline. I'm fallback-rendering `Intact` for any installed signed app (since Android refuses to install unsigned). The Cracked state activates **only** when a new install attempt's fingerprint differs from the stored one — needs a new DB column `expectedSigningFingerprint`. Flag for Phase 5. +7. **[A] Wonky squircle in Compose** — I'm using `AbsoluteRoundedCornerShape` with 4 distinct corner radii (single-axis). DESIGN.md's CSS uses 8-value border-radius (two axes per corner). The visible delta at our sizes (≤28.dp radius) is sub-pixel on @2x — accepting it. +8. **[A] Fonts at build time.** Fraunces variable + Inter Tight variable + JetBrains Mono — adding three fonts inflates APK ~600KB. Acceptable; Compose Resources supports font subsetting if we hit the wall. +9. **[B] Translate-the-app removal.** MIGRATION.md says no Translate UI but we ship 13 locales today (CLAUDE.md `core/presentation` + 13-locale strings). The translate plumbing in Tweaks is content-only. Confirm we keep the 13 user-facing UI locales (Settings > Language stays) and only forbid showing a "Translate this app" CTA. +10. **[A] Dynamic color migration.** Existing users with `AppTheme.DYNAMIC` get mapped to `NORD` on next launch and shown the one-time tooltip "We removed dynamic color — pick Nord, Cream, Forest, or Plum instead." First-launch dismissal stored in `TweaksRepository`. Flag if you want a different default. + +--- + +## 10. Quick-reference card (one-screen) + +- **Tokens** live in `tokens.json#palettes.{nord,cream,forest,plum}.{light,dark}` → `GhsTokens`. +- **Status colors** (palette-independent) live in `tokens.json#status.{freshness,wax,perm,trend}` → `GhsStatus`. +- **Shapes**: `chip 11/8`, `row 13/10`, `cardSm 15/11`, `card 18/14`, `cardLg 20/15`, `hero 24/18`, `heroLg 28/22`. Wonky variants for Primary CTA + Lead card + Search input. +- **Fonts**: Fraunces italic (names/headings), Inter Tight (body), JetBrains Mono (versions/hashes only). +- **Cookie at 3 places**: brand "G", user identity, Android active tab. +- **Squiggle**: one per section heading. No more. +- **Heartbeat**: only where it has space to label itself. Not in dense rows. +- **Wax-seal red**: the only place red is allowed to scream. +- **Per-app accent**: backend → topic → language → blue. +- **One filled button per surface.** +- **No emoji in UI chrome. No stock illustrations. No fake data.** + +End of UI-SPEC. diff --git a/.design/UX-ARCHITECTURE.md b/.design/UX-ARCHITECTURE.md new file mode 100644 index 000000000..0f4a3c134 --- /dev/null +++ b/.design/UX-ARCHITECTURE.md @@ -0,0 +1,817 @@ +# UX-ARCHITECTURE — Design System v2 refresh + +> Author: ArchitectUX. Companion: `UX-RESEARCH.md` (written by UX-Researcher in parallel — do not delete). +> Scope: technical plan only. **No Kotlin written yet.** Scaffold + sequencing. +> Source-of-truth handoff: `~/Downloads/handoff 4/` (DESIGN.md, tokens.json, MIGRATION.md, patterns.md, themes.md, design-system.md, silent-vocab.jsx, store-themed.jsx). + +--- + +## 0 · Current-state snapshot + +Findings from a read pass over the codebase. All paths root at repo. + +| Concern | File | Note | +|---|---|---| +| Material 3 theme entrypoint | `core/presentation/.../theme/Theme.kt:418` | `GithubStoreTheme(isDarkTheme, appTheme, fontTheme, isAmoledTheme)` → `MaterialExpressiveTheme`. | +| Color tokens | `core/presentation/.../theme/Color.kt`, `Theme.kt:17-414` | Five M3 schemes hand-rolled: Ocean, Purple, Forest, Slate, Amber. Single-axis (palette ⇆ scheme), dark-light selected by boolean. | +| Type | `core/presentation/.../theme/Type.kt` | Inter (regular sans) + JetBrains Mono already shipped under Compose Resources. **Fraunces is missing.** Custom-vs-system toggle exists. | +| Dynamic Material You | `Theme.android.kt`, `Theme.jvm.kt` | `expect/actual` `getDynamicColorScheme(dark)`. JVM = null. | +| Persistence | `core/data/.../repository/TweaksRepositoryImpl.kt:66-91` | KSafe-backed. Keys: `K_THEME` (`app_theme`), `K_IS_DARK` (`is_dark_theme`, tri-state nullable), `K_AMOLED`, `K_FONT`. | +| Domain enums | `core/domain/.../model/AppTheme.kt`, `FontTheme.kt` | `AppTheme = { DYNAMIC, OCEAN, PURPLE, FOREST, SLATE, AMBER }`. No mode enum — `null` Boolean encodes "system". | +| Repository "Themes" interface | none | The handoff calls it `ThemesRepository`; in this repo theme prefs live on `TweaksRepository`. We keep that name. | +| Font assets | `core/presentation/src/commonMain/composeResources/font/` | `inter_*.ttf`, `jetbrains_mono_*.ttf`. Loaded via `org.jetbrains.compose.resources.Font(Res.font.X)`. | +| Vector drawables | `core/presentation/.../composeResources/drawable/ic_platform_*.xml` | `.xml` vectors — Compose Resources `painterResource(Res.drawable.X)` already in use. | +| Local providers (existing) | `core/presentation/.../locals/Local{BottomNavigationHeight,ContentWidth,ScrollbarEnabled}.kt` | Pattern in place: `staticCompositionLocalOf` with `noLocalProvidedFor` defaults. We extend it, don't reinvent. | +| Sample card | `core/presentation/.../components/ExpressiveCard.kt:15` | Symmetric `RoundedCornerShape(32.dp)`. Needs asymmetric replacement. | + +**Implication.** We have to do two structural changes besides the visual refresh: + +1. **Two-axis theme model.** Today: 6 palettes × `Boolean?` dark. New: 4 palettes × 3-mode (LIGHT/DARK/SYSTEM). Old palettes (OCEAN, PURPLE, SLATE, AMBER, DYNAMIC) are decommissioned. A migration is required. +2. **Asymmetric shapes.** Compose's `RoundedCornerShape` accepts four corners but each corner is a single radius (x == y). The handoff uses elliptical radii for the wonky squircle (`20px 14px 22px 16px / 16px 22px 14px 20px`). Requires a custom `Shape`. + +--- + +## 1 · Token system + +### 1.1 Delivery: hand-written Kotlin objects (not codegen) + +**Decision: ship `Tokens.kt` as hand-written Kotlin in `core/presentation`.** Do **not** wire a Gradle task to generate from `tokens.json`. + +Justification: + +- `tokens.json` is ~270 lines, four palettes, frozen on v1. The "build step" amortises over churn, and we have none. +- Codegen breaks Kotlin's compile-time tools — `Color(0xFF...)` constants resolve to integers cleanly only when authored as Kotlin. Generated string-to-hex loses the `@Stable` guarantees and bloats DI. +- The handoff's `silent-vocab.jsx` already encodes thresholds (`freshnessOf`, `freshnessFraction`, `starTier`) and shape paths. Hand-porting forces the implementer to read the rule, not skim it. +- A future regen story: keep `tokens.json` checked in at `.design/tokens.json`. Add a one-off `./gradlew :core:presentation:checkTokens` Konsist/regex sanity that grep-asserts every hex in the JSON appears in `PaletteTokens.kt`. Cheap, no codegen. +- Build-step alternative considered: `build-logic/convention/.../TokensGenerationConventionPlugin.kt` that runs a `KotlinScript` reading the JSON. Rejected: extra build cost (Kotlin daemon spin), brittle to schema drift, and no consumer outside this one module. + +**Rule:** if a 5th palette ever ships, write it by hand. We never invent palettes (themes.md §"Disallowed combinations"). + +### 1.2 File layout + +Under `core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/`: + +``` +theme/ +├── tokens/ +│ ├── PaletteTokens.kt // data class PaletteColors + 8 instances +│ ├── StatusTokens.kt // freshness/wax/perm/trend (palette-independent) +│ ├── ShapeTokens.kt // asymmetric radius constants + WonkySquircleShape + CookieShape + Squiggle path +│ ├── SpacingTokens.kt // xxs..xxxl Dp +│ ├── TypeTokens.kt // TypeScale data class + roles +│ ├── MotionTokens.kt // durations + easings + Heartbeat periods +│ └── ThresholdTokens.kt // freshness/maintenance/stars lookup +├── locals/ +│ ├── LocalPalette.kt +│ ├── LocalStatusColors.kt +│ ├── LocalTypeScale.kt +│ ├── LocalShapes.kt +│ ├── LocalSpacing.kt +│ └── LocalMotion.kt +├── GhsTheme.kt // new entrypoint (replaces GithubStoreTheme) +├── Color.kt // KEEP — re-export for any non-migrated screen; mark @Deprecated +└── Theme.kt // KEEP wrapper that delegates GithubStoreTheme → GhsTheme during migration +``` + +`Color.kt`, `Theme.kt`, `Type.kt` stay alongside as a deprecation shim until phase 3+. Calls into the deprecated API compile but warn; new code consumes `GhsTheme` + `LocalPalette.current`. + +### 1.3 Data classes (sketch — not code yet, only contract) + +``` +@Immutable +data class PaletteColors( + val bg, surface, surface2, ink, ink2, outline, primary, tintP, + success, successT, danger, dangerT, shadow, +) // all androidx.compose.ui.graphics.Color + +@Immutable +data class StatusColors( + val freshness: FreshnessColors, // hot, fresh, warm, cool, dormant + val wax: WaxColors, // intact, cracked, open + val perm: PermColors, // low, moderate, high + val trend: TrendColors, // rising, flat, falling +) + +@Immutable +data class TypeScale( + val display: TextStyle, displaySm: TextStyle, headline: TextStyle, + val title: TextStyle, titleSm: TextStyle, + val body: TextStyle, bodySm: TextStyle, + val caption: TextStyle, label: TextStyle, mono: TextStyle, + val h3Warm: TextStyle, h3Meta: TextStyle, // editorial vs metadata variants +) + +@Immutable +data class GhsShapes( + val xs: Shape, sm: Shape, md: Shape, lg: Shape, xl: Shape, + val wonkySquircle: Shape, wonkySquircleAlt: Shape, wonkySquircleSearch: Shape, + val cookie: Shape, full: Shape, +) + +@Immutable +data class Spacing(val xxs, xs, s, sm, m, l, xl, xxl, xxxl: Dp) + +@Immutable +data class GhsMotion( + val quick: AnimationSpec, val medium: AnimationSpec, val slow: AnimationSpec, + val springSoft: SpringSpec, val springBouncy: SpringSpec, + val heartbeat: HeartbeatSpec, // scaleFrom 1.0, scaleTo 1.25, haloTo opacity 0 +) + +data class Thresholds( + val freshness: List, // {maxDaysInclusive, state, ringFraction, color} + val stars: List, // {minStars, tier} + val maintenance: List, // {maxDaysInclusive, state, heartbeatPeriodMs} +) +``` + +All marked `@Immutable` so Compose can skip recomposition when a CompositionLocal carries them unchanged. Status / Thresholds / Shapes are **constants** — only `PaletteColors` and (downstream) `TypeScale.color` change per state. + +--- + +## 2 · Composition locals + `GhsTheme` + +### 2.1 Provider entrypoint + +``` +@Composable +fun GhsTheme( + palette: Palette = Palette.NORD, + mode: ResolvedMode = ResolvedMode.LIGHT, // already-resolved (system → light/dark done upstream) + fontTheme: FontTheme = FontTheme.CUSTOM, + content: @Composable () -> Unit, +) +``` + +Inside, it: + +1. Resolves the `PaletteColors` for `(palette, mode)` from a frozen 4×2 map (8 instances). +2. Builds a Material 3 `ColorScheme` from those tokens (see §2.2 mapping). +3. Resolves the `TypeScale` (Fraunces + Inter Tight + JetBrains Mono, see §5) with current ink color baked into each `TextStyle`. +4. Provides composition locals + calls `MaterialExpressiveTheme(...)` so existing M3 widgets keep working. + +``` +CompositionLocalProvider( + LocalPalette provides paletteColors, + LocalStatusColors provides StatusColors.Constant, // palette-independent + LocalTypeScale provides typeScale, + LocalShapes provides GhsShapes.Default, + LocalSpacing provides Spacing.Default, + LocalMotion provides GhsMotion.Default, +) { + MaterialExpressiveTheme( + colorScheme = colorScheme, + typography = MaterialBridge.toM3Typography(typeScale), + shapes = MaterialBridge.toM3Shapes(GhsShapes.Default), + motionScheme = MotionScheme.expressive(), + content = content, + ) +} +``` + +### 2.2 Material 3 mapping + +Material widgets (sliders, dialogs, snackbar, ripple) read `MaterialTheme.colorScheme`. We map our tokens so they don't drift: + +| Our token | M3 slot | +|---|---| +| `bg` | `background`, `surface` (M3 conflates these in M3 Expressive), `surfaceContainerLowest` | +| `surface` | `surfaceContainer`, `surfaceContainerLow` | +| `surface2` | `surfaceContainerHigh`, `surfaceVariant` | +| `ink` | `onBackground`, `onSurface` | +| `ink2` | `onSurfaceVariant` | +| `outline` | `outline`, `outlineVariant` | +| `primary` | `primary`, `inversePrimary` (the latter via tonal flip per mode) | +| `tintP` | `primaryContainer`, `secondaryContainer` | +| `surface` (white fg) | `onPrimary` — but Cream `primary` (`#B8542C`) on white = 4.8:1 ✓. Forest light primary (`#6B8E5A`) on white = 3.0:1, fails AA. **Action:** for Cream + Forest light, use the palette `ink` as `onPrimary` (it's `#2B1F14` / `#2D3A2C` respectively — 7:1 against the primary). Document per-palette in PaletteTokens.kt. | +| `success` | `tertiary` (closest semantic match in M3) | +| `successT` | `tertiaryContainer` | +| `danger` | `error` | +| `dangerT` | `errorContainer` | + +**Why both layers?** M3 components don't know about our tokens. If a third-party Compose lib (Landscapist placeholders, Markdown renderer, navigation animations) reads `MaterialTheme.colorScheme.surface`, it should still get the right surface. Composition locals are for *our* code; M3 colorScheme is for *everyone else*. + +### 2.3 Locals API (read site) + +``` +object Ghs { + val palette @Composable get() = LocalPalette.current + val status @Composable get() = LocalStatusColors.current + val type @Composable get() = LocalTypeScale.current + val shapes @Composable get() = LocalShapes.current + val spacing @Composable get() = LocalSpacing.current + val motion @Composable get() = LocalMotion.current +} + +// Call site: +Text("Updated", style = Ghs.type.label, color = Ghs.palette.ink2) +Box(Modifier.background(Ghs.palette.tintP, Ghs.shapes.lg)) +``` + +`staticCompositionLocalOf` for everything (we never animate between palettes by interpolating — palette switches are a recomposition event, not an interpolation; see themes.md §"Mode-switching transitions" which crossfades the whole tree, not individual color values). + +--- + +## 3 · Asymmetric "wonky squircle" `Shape` + +### 3.1 Problem + +CSS: `border-radius: 20px 14px 22px 16px / 16px 22px 14px 20px;` — four corners, each with **different x and y radii** (elliptical). Compose: + +- `RoundedCornerShape` — same per-corner radius, x == y. +- `AbsoluteRoundedCornerShape` — also x == y per corner. +- `CutCornerShape` — wrong shape. + +No built-in elliptical-per-corner shape exists. We write `WonkySquircleShape : Shape`. + +### 3.2 Algorithm + +``` +class WonkySquircleShape( + val topLeftX: Dp, val topLeftY: Dp, + val topRightX: Dp, val topRightY: Dp, + val bottomRightX: Dp, val bottomRightY: Dp, + val bottomLeftX: Dp, val bottomLeftY: Dp, +) : Shape { + + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { + val (tlx, tly, trx, try_, brx, bry, blx, bly) = density.toPxAll(...) + val w = size.width; val h = size.height + val path = Path().apply { + moveTo(tlx, 0f) + lineTo(w - trx, 0f) + // top-right corner: ellipse arc from (w-trx, 0) → (w, try_) + arcTo( + rect = Rect(w - 2*trx, 0f, w, 2*try_), + startAngleDegrees = -90f, sweepAngleDegrees = 90f, forceMoveTo = false, + ) + lineTo(w, h - bry) + arcTo(rect = Rect(w - 2*brx, h - 2*bry, w, h), 0f, 90f, false) + lineTo(blx, h) + arcTo(rect = Rect(0f, h - 2*bly, 2*blx, h), 90f, 90f, false) + lineTo(0f, tly) + arcTo(rect = Rect(0f, 0f, 2*tlx, 2*tly), 180f, 90f, false) + close() + } + return Outline.Generic(path) + } +} +``` + +`Path.arcTo` with an elliptical bounding `Rect` (width ≠ height) draws an elliptical arc. This is the trick that closes the gap. + +Mirror for `LayoutDirection.Rtl`: swap (TL, TR) and (BL, BR) corners. + +### 3.3 Token wiring + +`ShapeTokens.kt` exposes named instances using the JSON values: + +``` +val WonkySquircleCard = WonkySquircleShape(tl=20.dp/16.dp, tr=14.dp/22.dp, br=22.dp/14.dp, bl=16.dp/20.dp) +val WonkySquircleCardAlt = WonkySquircleShape(...22/18, 16/24, 24/16, 18/22) +val WonkySquircleSearch = WonkySquircleShape(...24/18, 18/24, 26/20, 20/26) +``` + +**Notation:** `tl=20.dp/16.dp` = "top-left x-radius 20dp, y-radius 16dp" in CSS shorthand order. + +### 3.4 Fallback (graceful degradation) + +If `WonkySquircleShape` ever causes outline-clip artifacts on a target platform (e.g. Skia rasterisation glitch on JVM Linux), provide: + +``` +object Shapes { + val wonkySquircle: Shape = when { + DegradedShapes.enabled -> AbsoluteRoundedCornerShape( + topStart=18.dp, topEnd=14.dp, bottomEnd=20.dp, bottomStart=16.dp, + ) // symmetric per corner — loses ~5% of the wonky feel + else -> WonkySquircleShape(...) + } +} +``` + +Gated by a build flag. Default = full shape. Use the fallback only if we hit a rasterisation bug in QA. + +### 3.5 The diagonal asymmetry rule (design-system.md §6.2) + +For all *non-wonky* asymmetric corners (rows, chips, buttons) the rule is symmetric-per-corner with two values diagonally: + +``` +RoundedCornerShape(topStart=L, topEnd=S, bottomEnd=L, bottomStart=S) +``` + +Built-in `RoundedCornerShape` handles this. No custom shape needed for `xs/sm/md/lg/xl`. Wonky squircle is **only** for primary CTAs and lead/hero cards (CSS comment: *"feels hand-shaped"*). + +--- + +## 4 · `CookieShape` + `Squiggle` + +### 4.1 CookieShape (clippable) + +`tokens.json` ships: +- `viewBox: "0 0 100 100"` +- `path: "M50 4 C 62 4 66 12 76 12 C 86 12 91 22 91 32 C 95 40 100 50 94 58 C 96 70 90 82 80 86 C 72 90 64 96 54 96 C 44 96 36 95 26 92 C 16 90 10 80 8 70 C 4 62 0 54 6 46 C 6 34 12 22 22 18 C 32 12 38 4 50 4 Z"` + +This is ~12 cubic Bezier segments. Two options: + +**Option A — hand-translate.** Map each `C x1 y1, x2 y2, x y` to `path.cubicTo(x1*sx, y1*sy, x2*sx, y2*sy, x*sx, y*sy)` where `sx = size.width / 100f`, `sy = size.height / 100f`. ~12 lines of code. Done once, frozen. + +**Option B — SVG path parser.** Tokenize the `d` string, dispatch by command (`M`, `C`, `T`, `Q`, `L`, `Z`). Useful if more SVG paths arrive. Squiggle uses `Q` and `T` (quadratic + smooth quadratic), so the parser must handle those if we want one parser for both. + +**Decision: Option A.** Two paths, frozen tokens, no need for a parser. Hand-translate both into `Path` builders. Document the source `d` string in a KDoc/comment so the next maintainer knows where it came from. + +``` +object CookieShape : Shape { + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { + val sx = size.width / 100f + val sy = size.height / 100f + val p = Path().apply { + moveTo(50f*sx, 4f*sy) + cubicTo(62f*sx, 4f*sy, 66f*sx, 12f*sy, 76f*sx, 12f*sy) + cubicTo(86f*sx, 12f*sy, 91f*sx, 22f*sy, 91f*sx, 32f*sy) + // ... 10 more + close() + } + return Outline.Generic(p) + } +} +``` + +Then `Modifier.clip(CookieShape)` works on any `Box` / `Image` / brand mark. + +Sizes specified (design-system.md): brand mark, profile avatar, active bottom-nav tab. All three accept `Modifier.size(40.dp)` / `64.dp` / `28.dp` and the shape scales because of `size.width / 100f`. + +### 4.2 Squiggle (decorative, not clippable) + +``` +viewBox: "0 0 40 5" +path: "M1 3 Q 5 0.5, 9 3 T 17 3 T 25 3 T 33 3 T 39 3" +stroke: "1.6px", opacity: 0.6, color: "primary" +``` + +Not a clip shape (it's an underline). Render with `Canvas`: + +``` +@Composable +fun Squiggle(modifier: Modifier = Modifier, color: Color = Ghs.palette.primary) { + Canvas(modifier.size(width=40.dp, height=5.dp)) { + val p = Path().apply { + moveTo(1f*sx, 3f*sy) + quadraticBezierTo(5f*sx, 0.5f*sy, 9f*sx, 3f*sy) + // smooth-quadratic: reflect previous ctrl point. T x,y == Q (2*prevEnd - prevCtrl), x, y + quadraticBezierTo(13f*sx, 5.5f*sy, 17f*sx, 3f*sy) + quadraticBezierTo(21f*sx, 0.5f*sy, 25f*sx, 3f*sy) + quadraticBezierTo(29f*sx, 5.5f*sy, 33f*sx, 3f*sy) + quadraticBezierTo(37f*sx, 0.5f*sy, 39f*sx, 3f*sy) + } + drawPath(p, color.copy(alpha=0.6f), + style = Stroke(width=1.6.dp.toPx(), cap=StrokeCap.Round)) + } +} +``` + +(The `T` reflection is pre-computed above — Compose `Path` has no smooth-quadratic primitive.) + +--- + +## 5 · Typography + +### 5.1 Three fonts, one resource bundle + +Already on disk: +- `inter_{light,regular,medium,semi_bold,bold,black}.ttf` +- `jetbrains_mono_{light,regular,medium,semi_bold,bold}.ttf` + +Missing: +- **Fraunces** — italic 500/600/700, with optical-size axis 9..144 (the JSON imports it variable). We will ship **static italic TTFs** (3 weights × italic) for app-size correctness; variable TTF parses on Android API 26+ and JVM (Skia ≥ M85) but is heavier and Compose's `Font(...)` doesn't expose axis controls in the Resources API today. Static is safe. +- **Inter Tight** — currently we ship "Inter" (the wider one). DESIGN.md §3.4 forbids it ("Inter Tight reads tighter and pairs better with Fraunces"). Replace `inter_*.ttf` with `inter_tight_*.ttf` (400/500/600/700 + a 800 variant for the rare display-weight case). + +Drop into `core/presentation/src/commonMain/composeResources/font/` (same place as today). Compose Resources generates `Res.font.X` accessors for `commonMain` consumption — **no `expect/actual` needed**. The existing `Type.kt:12-30` pattern (using `org.jetbrains.compose.resources.Font(...)`) is the right pattern; we extend it. + +### 5.2 Multi-script policy + +From `MIGRATION.md`: Inter Tight covers Latin + Cyrillic only. Fraunces is Latin/Cyrillic only. + +**For Latin/Cyrillic:** Fraunces (display) + Inter Tight (body) + JetBrains Mono (code). + +**For other scripts:** fall back via `FontFamily` composition. Compose `FontFamily` accepts multiple `Font` entries with the same weight; if a glyph isn't in the first font, the platform falls back through the list. We will: + +1. Ship Noto Sans variants in `composeResources/font/`: `noto_sans_jp`, `noto_sans_sc`, `noto_sans_tc`, `noto_sans_kr`, `noto_sans_devanagari`, `noto_sans_arabic`, `noto_sans_hebrew`. Regular + Bold only — that's enough for headlines + body. +2. Build family with platform-aware ordering. **On Android** the platform's `Typeface` does its own script fallback against system fonts; just provide the Latin fonts and let Android do the rest (Android system fonts cover everything). **On JVM** the situation is worse — JDK font fallback is best-effort, headless Linux may have no CJK. Ship the Noto TTFs as `Font(Res.font.noto_sans_jp_regular)` and **embed them in `FontFamily` after the Latin fonts**. This costs ~5MB per Noto family but guarantees no tofu on any platform. + +`FontFamily` declaration: +``` +val SansBody = FontFamily( + Font(Res.font.inter_tight_regular, FontWeight.Normal), + Font(Res.font.inter_tight_medium, FontWeight.Medium), + Font(Res.font.inter_tight_semi_bold, FontWeight.SemiBold), + Font(Res.font.inter_tight_bold, FontWeight.Bold), + // Fallbacks for non-Latin scripts (Compose picks based on glyph coverage) + Font(Res.font.noto_sans_jp_regular, FontWeight.Normal), + Font(Res.font.noto_sans_sc_regular, FontWeight.Normal), + Font(Res.font.noto_sans_kr_regular, FontWeight.Normal), + Font(Res.font.noto_sans_devanagari_regular, FontWeight.Normal), + Font(Res.font.noto_sans_arabic_regular, FontWeight.Normal), + Font(Res.font.noto_sans_hebrew_regular, FontWeight.Normal), + // (matching bolds...) +) +``` + +**Open question (§12-Q3):** is the ~30MB APK size hit (Noto × 7 scripts × 2 weights) acceptable, or do we prefer Android-only system fallback + accept JVM tofu on minority scripts? + +### 5.3 Italic Fraunces — "App identity" rule + +DESIGN.md §3.3: "Italic Fraunces only for app/screen identity. Don't italicize body or buttons." Practical implication: the `TypeScale` only puts `FontStyle.Italic` on `display`, `displaySm`, `headline`, and `h3Warm`. Body/title/caption use upright. + +### 5.4 Tabular numbers + +DESIGN.md §5.3: "Numbers use Inter Tight tabular (`fontVariantNumeric: 'tabular-nums'`)". In Compose: `TextStyle(fontFeatureSettings = "tnum")`. Bake this into `TypeScale.caption`, `body`, `bodySm`, `mono` — anywhere a star count, version, or download number renders. + +--- + +## 6 · Theme persistence — schema migration + +### 6.1 New domain model + +`core/domain/.../model/Palette.kt`: +``` +enum class Palette { NORD, CREAM, FOREST, PLUM ; + companion object { fun fromName(n: String?) = entries.find { it.name == n } ?: NORD } +} +``` + +`core/domain/.../model/ThemeMode.kt`: +``` +enum class ThemeMode { LIGHT, DARK, SYSTEM ; + companion object { fun fromName(n: String?) = entries.find { it.name == n } ?: SYSTEM } +} +``` + +`AppTheme.kt`: **deprecated** with `@Deprecated("Replaced by Palette + ThemeMode")`. Keep the enum compiled so existing code on `feature/tweaks` still resolves until phase 5. Provide an extension `AppTheme.toPalette(): Palette` for the migration path (table below). + +### 6.2 TweaksRepository surface change + +``` +// new — added alongside the old methods +fun getPalette(): Flow +suspend fun setPalette(palette: Palette) +fun getThemeMode(): Flow +suspend fun setThemeMode(mode: ThemeMode) + +// old — kept until phase 5, then removed in one commit +@Deprecated fun getThemeColor(): Flow +@Deprecated suspend fun setThemeColor(theme: AppTheme) +@Deprecated fun getIsDarkTheme(): Flow +@Deprecated suspend fun setDarkTheme(isDarkTheme: Boolean?) +``` + +`getAmoledTheme`/`setAmoledTheme` survives — AMOLED is a sub-mode of DARK and we keep that knob (§12-Q5). + +### 6.3 Persistence keys + +Add new KSafe keys in `TweaksRepositoryImpl.kt:447`: +``` +private const val K_PALETTE = "palette_v2" +private const val K_THEME_MODE = "theme_mode_v2" +``` + +Old keys (`K_THEME = "app_theme"`, `K_IS_DARK = "is_dark_theme"`) stay on disk. On first read of `getPalette()`/`getThemeMode()`: + +``` +override fun getPalette(): Flow = flow { + migrationDeferred.await() + val raw: String = ksafe.safeGet(K_PALETTE, "") + if (raw.isEmpty()) { + // one-shot migration from legacy K_THEME + val legacy: String = ksafe.safeGet(K_THEME, "") + val migrated = mapLegacyAppThemeToPalette(legacy) // see table below + ksafe.safePut(K_PALETTE, migrated.name) + emit(migrated) + } + emitAll(ksafe.safeGetFlow(K_PALETTE, Palette.NORD.name).map { Palette.fromName(it) }) +} +``` + +### 6.4 Legacy → new palette map + +| Old `AppTheme` | New `Palette` | Notes | +|---|---|---| +| `OCEAN` | `NORD` | Nord is the cool-blue default. | +| `DYNAMIC` | `NORD` | Material You removed — themes.md §"Don't let dynamic color override". | +| `PURPLE` | `PLUM` | Closest mood. | +| `FOREST` | `FOREST` | Direct map. | +| `SLATE` | `NORD` | Cool grey → nord (no muted-grey palette in v2). | +| `AMBER` | `CREAM` | Warm orange → cream. | + +`getIsDarkTheme(): Flow` → `getThemeMode(): Flow`: +- `null` → `SYSTEM` +- `false` → `LIGHT` +- `true` → `DARK` + +Migrated once on first read, then `K_IS_DARK` is left in place (read-only, no harm). + +### 6.5 Resolving "system" mode + +`GhsTheme` takes `ResolvedMode = LIGHT | DARK` (already resolved). The resolution happens once near the root of the tree: + +``` +// inside MainViewModel or AppNavigation +val mode by tweaks.getThemeMode().collectAsState(ThemeMode.SYSTEM) +val systemDark = isSystemInDarkTheme() // Compose primitive +val resolved = when (mode) { + ThemeMode.LIGHT -> ResolvedMode.LIGHT + ThemeMode.DARK -> ResolvedMode.DARK + ThemeMode.SYSTEM -> if (systemDark) ResolvedMode.DARK else ResolvedMode.LIGHT +} +GhsTheme(palette, resolved, fontTheme) { AppNavigation() } +``` + +`isSystemInDarkTheme()` is multiplatform (commonMain). Single source of resolution, no scattered branching. + +--- + +## 7 · Per-app accent storage + +### 7.1 Domain model + +`core/domain/.../model/AppAccent.kt`: +``` +@Immutable +data class AppAccent( + val c: Color, // saturated accent (text, icon, recommended-pill foreground) + val lightTint: Color, // light-mode tint surface + val darkTintAlpha: Float = 0.20f, // dark-mode = c.copy(alpha=darkTintAlpha) over dark surface +) { + fun tintFor(mode: ResolvedMode): Color = + if (mode == ResolvedMode.DARK) c.copy(alpha = darkTintAlpha) else lightTint +} +``` + +### 7.2 Resolution chain (`core/data/util/AppAccents.kt`) + +``` +object AppAccents { + fun forRepo( + backendAccent: String?, // hex from backend (future — not wired yet) + topics: List, + primaryLanguage: String?, + ): AppAccent { + backendAccent?.let { return AppAccent.fromHex(it) } + topics.firstNotNullOfOrNull { TOPIC_ACCENTS[it] }?.let { return it } + primaryLanguage?.let { LANGUAGE_ACCENTS[it.lowercase()] }?.let { return it } + return FALLBACK_BLUE + } +} +``` + +**Resolution order** (themes.md): +1. Backend-supplied hex +2. Topic match (table below) +3. Language match (Kotlin→purple, Rust→amber, Go→sage, Swift→orange, Python→amber, JS/TS→sage, …) +4. Fallback `FALLBACK_BLUE = AppAccent(c=#5E81AC, lightTint=#D8E1EC)` + +Tables live as `val TOPIC_ACCENTS: Map` and `val LANGUAGE_ACCENTS: Map` in the same file. ~30 entries total. + +### 7.3 Where it lives on the data path + +**Recommendation: derive at the UI mapper layer, not at the repo data class.** + +Reasoning: +- `GithubRepoSummary` (`core/domain/.../model/GithubRepoSummary.kt`) and `DiscoveryRepositoryUi` are domain models. Accent is **presentation-only data** — it depends on the palette mode (light/dark tint) and on visual tokens. +- The existing pattern (`GithubRepoSummaryMappers.kt`, `GithubUserMappers.kt`) maps domain → UI in `core/presentation/.../utils/`. Add a `repoAccent: AppAccent` field to `GithubRepoSummaryUi` and resolve it in the mapper. +- **Not persisted.** Same `(topics, language)` deterministically resolves to the same accent — recomputation is free, caching adds invalidation bugs. + +**Exception:** if the backend ever returns a per-repo accent (DESIGN.md §2.4 mentions "color-thief on avatar"), that hex needs persistence to avoid recomputing on every fetch. Add a single nullable column to the existing repos table at that point — **not in this overhaul.** Defer to a future phase (§12-Q6). + +### 7.4 UI-side cache + +`Modifier.background(accent.tintFor(mode), shape)` recomputes per recomposition but the math is `Color.copy(alpha=…)` — single allocation, irrelevant. No memoisation needed. + +--- + +## 8 · Module layout for silent vocabulary + +**Decision: single module — `core/presentation/.../vocabulary/`.** + +Reasons: +- Every feature consumes these primitives (Home feed card uses FreshnessRing; Library row uses Heartbeat; Detail screen uses WaxSeal + VersionStack). A new module forces every feature to add a dependency; we already pay that cost on `core/presentation`. +- Primitives are stateless, take `Modifier` + theme tokens. They don't need their own data/domain layer. +- ~14 files, ~1500 lines total. Below the threshold where a new module pays off. + +Files: + +``` +core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/ +├── FreshnessRing.kt // squircle icon tile + draining ring; takes (daysSinceRelease, accent, avatarUrl?) +├── Heartbeat.kt // breathing dot, period from maintenance.heartbeat_period_s +├── StarTier.kt // 1–5 Michelin stars from log buckets +├── WaxSeal.kt // intact/cracked/open — three SVG glyphs via Path +├── VersionDelta.kt // patch dot / minor 2-dots / major bar+slash +├── VersionStack.kt // 1..7 stacked bars for skipped versions +├── PermDot.kt // green/amber/red dot + optional halo ring +├── PlatformGlyph.kt // android/windows/macos/linux — uses existing ic_platform_*.xml as Vector + dashed-outline variant +├── TopicGlyph.kt // 12 topic pictograms — Canvas-drawn from silent-vocab.jsx paths +├── SignalBars.kt // mirror strength, 0–4 bars +├── DownloadWeight.kt // log10 sized dot +├── LicensePosture.kt // copyleft © tile vs permissive · tile +├── Squiggle.kt // (lives here, not in theme/, since it's vocabulary-decorative) +└── CookieBrand.kt // brand mark composable using CookieShape from theme/ +``` + +**`Heartbeat.kt` perf note:** infinite animation, common in lists. Use `rememberInfiniteTransition` + `animateFloat`. Pause when not visible via `isInLazyListVisibleRange()` helper or `LocalLifecycleOwner`-aware composable. See §11 Risk-4. + +**Platform glyphs:** we already have `ic_platform_{android,windows,macos,linux}.xml`. Convert PlatformGlyph to a wrapper that picks between `painterResource(...)` for `on=true` and a Canvas-drawn dashed variant for `on=false`. Don't ship new platform SVGs. + +--- + +## 9 · Resource pipeline + +Compose Multiplatform 1.10.3 (per `gradle/libs.versions.toml`) supports two viable paths for vector content: + +**Path A — Compose Resources `painterResource(Res.drawable.foo)` on Android-XML vector drawables.** +- We already use this for `ic_platform_*.xml`. Works on Android (native vector drawable) + JVM (parsed by Compose Resources at compile time into ImageVector). +- Limitation: only Android-XML vector format. SVG-native is not supported. +- Good for: icons we'd otherwise ship as Material Symbols / custom platform glyphs. + +**Path B — `ImageVector.Builder` Kotlin DSL.** +- Build vectors at runtime from path data. +- Good for: small, dynamic vectors driven by data (e.g. WaxSeal — three states, ~5 path segments each; TopicGlyph — 12 distinct sub-paths driven by `kind: String`). + +**Decision:** **Path B for vocabulary primitives** (because the JSX uses tiny inline SVGs that compose better as `Canvas` or `ImageVector.Builder` than as 12 separate XML files), **Path A for everything else** (top-bar icons, platform glyphs we already ship, etc.). + +**No raster icons.** Ship no new PNGs. The existing `app_icon.png` stays, but topic glyphs / vocabulary primitives are all vector-only. + +For Material Symbols Rounded (design-system.md §12) — we don't bundle Material Symbols today. Either add the `androidx.compose.material:material-icons-extended` dependency (`Icons.Rounded.Home`, etc.) or ship Material Symbols as a font file (`MaterialSymbolsRounded.ttf`, ~600KB, all icons addressable via codepoint). **Prefer the font file** — `icons-extended` is ~10MB on Android baseline APK, the font is 600KB and addressed by `Text(text="", fontFamily=MaterialSymbolsRounded)`. Decided downstream during phase 1. + +--- + +## 10 · Sequencing + acceptance criteria + +Mapped to MIGRATION.md but adapted for our actual modules (no "Library" module → we have `feature/apps`, `feature/favourites`, `feature/starred`). + +### Phase 0 — Tokens + Theme scaffolding + +**Deliverable:** Every token (palette × mode, type, shape, spacing, motion, thresholds, status) reachable via `Ghs.X`/`MaterialTheme.X` from anywhere. Both theme entrypoints work side-by-side. + +**Files touched (new):** +- `core/presentation/.../theme/tokens/{Palette,Status,Shape,Spacing,Type,Motion,Threshold}Tokens.kt` +- `core/presentation/.../theme/locals/Local{Palette,StatusColors,TypeScale,Shapes,Spacing,Motion}.kt` +- `core/presentation/.../theme/GhsTheme.kt` +- `core/presentation/src/commonMain/composeResources/font/fraunces_{500,600,700}_italic.ttf` +- `core/presentation/src/commonMain/composeResources/font/inter_tight_{400,500,600,700}.ttf` +- `core/presentation/src/commonMain/composeResources/font/noto_sans_{jp,sc,tc,kr,devanagari,arabic,hebrew}_{regular,bold}.ttf` (open question §12-Q3) +- `core/domain/.../model/Palette.kt`, `ThemeMode.kt`, `AppAccent.kt`, `ResolvedMode.kt` +- `core/data/util/AppAccents.kt` + +**Files touched (modified):** +- `core/domain/.../repository/TweaksRepository.kt` — add `getPalette()`, `setPalette()`, `getThemeMode()`, `setThemeMode()`. Deprecate old four. +- `core/data/.../repository/TweaksRepositoryImpl.kt` — add `K_PALETTE`, `K_THEME_MODE`, migration on first read. +- `composeApp/.../app/Main.kt` (or wherever `GithubStoreTheme {}` is called) — branch to `GhsTheme` once palette is wired. + +**Definition of done:** +- `./gradlew :core:presentation:compileCommonMainKotlinMetadata` green. +- `./gradlew :composeApp:assembleDebug` green. +- Launching the app on Nord light + Nord dark + Cream light renders the existing UI without colour regressions (because we map M3 slots faithfully). +- Toggling Palette via a temporary debug menu cycles through all 4 palettes and persists across app restart. +- Fraunces renders in a temporary `Text(...)` call site without tofu on Android + JVM. +- Migration: a user with `K_THEME = "AMBER"` + `K_IS_DARK = true` on disk launches and lands on `Palette.CREAM` + `ThemeMode.DARK`. + +### Phase 1 — Silent vocabulary primitives + +**Deliverable:** All 14 primitives from §8 callable, each renders correctly in all 8 (palette × mode) combinations. + +**Files touched (new):** +- `core/presentation/.../vocabulary/*.kt` (14 files, see §8) +- One Compose Preview composable per primitive (Android only — Previews work there) under `core/presentation/src/androidMain/...`. + +**Definition of done:** +- Visual diff against `silent-vocab.jsx` reference. Each primitive matches CSS reference within ~5% tolerance. +- `Heartbeat` does not run animations when off-screen (LazyList scroll verification). +- Build green; lint green. + +### Phase 2 — Reusable cards, chips, buttons, rows + +**Deliverable:** `RepoCard`, `AppRow`, `SetRow`, `Chip`, `GhsButton`, `Section`, `BottomNav`, `TopBar`, `IconShell`, `InstallPanel`, `UpdateBanner`, `IntegrityCard`, `IdentityCard`, `ConnectCard` per design-system.md §10. + +**Files touched:** +- `core/presentation/.../components/v2/{RepoCard,AppRow,SetRow,Chip,GhsButton,Section,BottomNav,TopBar,IconShell,InstallPanel,UpdateBanner,IntegrityCard,IdentityCard,ConnectCard}.kt` + +Existing `ExpressiveCard`, `GithubStoreButton`, `RepositoryCard` stay; new components live in `components/v2/`. Phase 3+ swaps consumers; old files removed in a final cleanup commit per migration playbook §"Behaviour parity over visual parity". + +**Definition of done:** +- Sample screen wires every new component into a single scrollable demo (under `feature/tweaks` as a hidden debug entry, or under a new `feature/dev-profile` debug screen). +- Each component takes only tokens + content slots — zero hex/dp literals (Konsist check is overkill; eyeball + grep). + +### Phase 3 — Home feed migration + +**Deliverable:** `feature/home` swap to v2 components. Behaviour parity, visual replacement. + +**Files touched:** +- `feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt` and child composables +- ViewModel left alone unless data fields are missing (per-app accent resolution moves to the UI mapper — see §7.3). + +**DoD:** Home renders cards in Nord light + dark identically to handoff `home.jsx`. Existing acceptance test in `composeApp/.../HomeScreen*Test.kt` (if any) passes. + +### Phase 4 — Apps / Favourites / Starred (the "Library" trio) + +We don't have a single Library module. The handoff "Library" collapses three of ours into one screen logically — but **don't merge the modules**. Migration plan: + +- `feature/apps` is the primary visual surface; takes the "Library" treatment (sections: Updates, Installed, Recently used). +- `feature/favourites` and `feature/starred` get re-skinned with the same `AppRow` component but keep their own routes (the IA collapse from MIGRATION.md is a UX choice; the module collapse is not — too risky for one PR). +- The bottom-nav 4→3 reduction (per MIGRATION.md §"Recommended order #6") is **deferred to phase 7**. + +**Files touched:** `feature/apps/presentation`, `feature/favourites/presentation`, `feature/starred/presentation`. + +**DoD:** All three render with v2 components. Updates badge count visible on the Apps tab. + +### Phase 5 — Profile + Settings (Tweaks) + +**Deliverable:** `feature/profile`, `feature/tweaks` migrated. Tweaks gets the new **Palette + ThemeMode** two-axis picker UI. AMOLED toggle remains under "Dark mode" group. + +**Files touched:** `feature/profile/presentation/*`, `feature/tweaks/presentation/*`. + +**DoD:** Old `AppTheme` enum unused in UI code; remaining references are in the deprecated repo methods + the migration table. + +### Phase 6 — Details screen + +**Deliverable:** `feature/details` — the highest-fidelity screen, per MIGRATION.md. + +**Files touched:** `feature/details/presentation/*`. + +**DoD:** Hero block uses Fraunces displaySm + FreshnessRing on the app icon; install panel uses WonkySquircle; Integrity card uses `successT` background. + +### Phase 7 — Bottom nav reduction + remaining screens + +**Deliverable:** +- Bottom nav 4→3 (Home, Library-as-Apps, Profile) + detached search FAB. +- `feature/search`, `feature/auth`, `feature/dev-profile`, "What's new" sheet, mirror picker, external import wizard. +- Onboarding tooltip ("Settings moved to Profile") on first launch post-upgrade (`tweaksRepo.firstLaunchAfterV2`). + +**DoD:** All screens consume `GhsTheme`. Zero references to `GithubStoreTheme()` outside the deprecation shim. + +### Phase 8 — Deprecation cleanup + +**Deliverable:** Delete `AppTheme.kt`, deprecated `TweaksRepository` methods, old `Theme.kt` schemes, `Color.kt` ocean-blue constants, `ExpressiveCard.kt`, `GithubStoreButton.kt`, `RepositoryCard.kt`. The deprecation shim `Theme.kt → GhsTheme` is removed. + +**DoD:** `grep -rn 'AppTheme\|GithubStoreTheme\|isDarkTheme: Boolean' core feature` returns zero hits. `:composeApp:assembleDebug` green. + +--- + +## 11 · Risk register + +Likelihood × impact rated 1–5. Score = L × I. + +| # | Risk | L | I | Score | Mitigation | +|---|---|---|---|---|---| +| 1 | `WonkySquircleShape` arc math wrong → visible artifacts (over/under-shoots, mis-aligned corners) | 3 | 3 | 9 | Implement with Skia debug overlay turned on in a dev preview screen. Diff against a CSS reference rendered at 2x in a screenshot. Have the `AbsoluteRoundedCornerShape` fallback (§3.4) flag-ready. | +| 2 | Fraunces italic doesn't render on JVM Linux (system font fallback to bold-not-italic) | 2 | 4 | 8 | Ship the TTF in `composeResources/font/` (bundled). Compose Resources packages fonts into the jar; Skia loads from the bundle, not the system. Same path as `inter_*.ttf` today. | +| 3 | Theme migration loses user setting → user re-themes app on first v2 launch | 2 | 4 | 8 | Migration is idempotent (only runs when `K_PALETTE` is empty). Add a Logcat/Kermit log line on each migration so support can verify. Keep `K_THEME`/`K_IS_DARK` on disk untouched in case we ever need to rollback. | +| 4 | `Heartbeat` infinite animation in a list of 50 cards eats CPU + battery | 4 | 3 | 12 | Always-on `InfiniteTransition` is per-composable: it pauses when the composable leaves composition. **In a `LazyColumn`, recycled items leave composition automatically.** Bigger concern: 8 visible cards on screen × continuous repaint at 60 Hz. Mitigations: (a) tie the transition's `targetValue` keyframes to discrete steps so the GPU has few invalidations; (b) gate behind `LocalLifecycleOwner` paused → no animation; (c) provide a `Tweaks → Reduce motion` switch that returns a static dot (§12-Q4). | +| 5 | M3 colorScheme mapping mismatch — third-party widget (Markdown renderer, Landscapist placeholder) renders wrong colour on a dark Cream theme | 3 | 2 | 6 | Audit each consumer (`markdown-renderer`, `landscapist-coil`, navigation transition) for which `colorScheme.X` slots they read. Document in `PaletteTokens.kt` next to each mapping. Snapshot test the markdown renderer pass on all 8 (palette × mode) variants. | +| 6 | Per-app accent resolution is non-deterministic (topic list arrives in different order from backend) | 2 | 3 | 6 | `topics.firstNotNullOfOrNull { TOPIC_ACCENTS[it] }` — if backend reorders, accent changes. Mitigation: sort topics alphabetically before resolution. Or use the explicit GitHub API ordering (which is creation order — stable across calls). Note in `AppAccents.kt` docstring. | +| 7 | Noto fallback adds ~30 MB to APK | 4 | 2 | 8 | Open question §12-Q3 — decision needed. If we drop Noto, Android system fallback covers it (Android has CJK + Devanagari fonts in /system/fonts); JVM gets tofu on user-installed-only systems. Compromise: ship Noto on JVM only (split sourceSet), Android relies on system. | +| 8 | Two-axis picker UI breaks muscle memory — existing users hit `Tweaks → Theme` and find new layout | 2 | 2 | 4 | Tooltip on first launch per MIGRATION.md §"Risky areas". Keep the section title as "Theme" not "Appearance" so search-by-label still finds it. | + +Top three by score: #4 Heartbeat perf, #1 wonky-squircle math, #7 Noto size. + +--- + +## 12 · Open questions for the user + +These need a decision before phase 0 begins (or, marked clearly, during the phase). One answer per number. + +**Q1 — Fraunces font axis.** Ship static italic TTFs (3 weights × italic, ~600KB) or the variable TTF (~250KB but no axis API in Compose Resources)? Recommend static. + +**Q2 — Inter Tight replacement of Inter.** DESIGN.md §3.4 forbids "Inter" (the wider one). Today we ship Inter. Confirm: replace `inter_*.ttf` files with `inter_tight_*.ttf` (same `Res.font.inter_X` keys, different file)? Or rename keys to `inter_tight_X` and migrate `Type.kt:21-30` references? Recommend rename — keeps git history clear. + +**Q3 — Noto Sans bundling.** Ship 7 Noto Sans families (JP/SC/TC/KR/Devanagari/Arabic/Hebrew) × 2 weights = 14 TTFs, ~30 MB APK. Or rely on Android system fallback (covers everything) + accept JVM tofu on minority scripts? Or split sourceSet: Android = no Noto, JVM = bundle Noto? Recommend the split approach (Q3-C). + +**Q4 — "Reduce motion" Tweak.** Add a new boolean to `TweaksRepository` for users who want `Heartbeat` to render as a static dot? Or rely on the OS-level reduce-motion setting (Android `Settings.Global.TRANSITION_ANIMATION_SCALE`, JVM has none)? Recommend new in-app toggle — OS signal is unreliable and the JVM has no equivalent. + +**Q5 — AMOLED black mode in v2.** Today `getAmoledTheme()` forces surfaces to true black when dark. Keep this as a sub-mode of `ThemeMode.DARK` (rendered as a toggle in Tweaks, not a third mode)? Recommend yes — three top-level modes (LIGHT/DARK/SYSTEM) + a "Pure black surfaces when dark" sub-toggle. + +**Q6 — Backend per-repo accent.** DESIGN.md §2.4 says "When backend doesn't supply one, derive from the dominant color of the avatar (color-thief style) and store it." Today the backend doesn't supply one and we don't color-thief. Confirm: in this overhaul, **don't** add color-thief. Resolve accent client-side from topics → language → fallback only. Backend-supplied accent is a future feature. + +**Q7 — Material You / dynamic colour.** themes.md §"Disallowed combinations" forbids Material You on Android 12+ overriding our palettes. Today we ship `AppTheme.DYNAMIC`. Migration table (§6.4) folds `DYNAMIC → NORD`. Confirm: drop `AppTheme.DYNAMIC` entirely, no opt-in to dynamic colour anywhere? (Recommend yes.) + +**Q8 — Two-axis Tweaks UI breakage.** The new "Palette" + "Theme mode" picker is a different layout than the current single-list theme selector. Acceptable to break the current Tweaks visual on the day we ship phase 5? Or do we need a one-release transition where the old screen still exists behind a debug flag? Recommend: ship clean break + add the one-time tooltip (§11 Risk-8). + +**Q9 — Module for vocabulary.** I recommended single module (`core/presentation/vocabulary/`) over a new `core/vocabulary/` module. Confirm? A new module would force `core/presentation` to depend on it and every feature already depends on `core/presentation`, so no consumer benefit. + +**Q10 — Where does the palette+mode resolution live?** Recommended (§6.5) at the root of `AppNavigation` via `MainViewModel`. Alternative: introduce a tiny `ThemeViewModel` that exposes a `StateFlow` derived from palette + mode + system-dark + amoled. Recommend `MainViewModel` extension — it already exists, less DI churn. + +--- + +## Summary — what ArchitectUX is on the hook for in phase 0 + +Read this section before opening a new chat to start phase 0. + +1. Write `core/presentation/.../theme/tokens/{Palette,Status,Shape,Spacing,Type,Motion,Threshold}Tokens.kt` from `tokens.json`. +2. Write `WonkySquircleShape : Shape` + `CookieShape : Shape` from §3.2 and §4.1. +3. Add Fraunces + Inter Tight (+ Noto per Q3) to `composeResources/font/`. +4. Add `Palette`, `ThemeMode`, `ResolvedMode`, `AppAccent` enums + data classes. +5. Add `getPalette`/`setPalette`/`getThemeMode`/`setThemeMode` to `TweaksRepository` + impl + migration. +6. Write `GhsTheme.kt` + 6 composition locals. +7. Wire `GhsTheme` into `composeApp/.../app/Main.kt` (or the top-level theming call site) behind a runtime flag so we can flip between old + new during phases 1–7. +8. One screenshot per (4 palettes × 2 modes) of a temporary debug screen showing a card, a button, an FAB, and a status row — to verify the colour mapping landed. + +End. diff --git a/.design/UX-AUDIT.md b/.design/UX-AUDIT.md new file mode 100644 index 000000000..82ca9617c --- /dev/null +++ b/.design/UX-AUDIT.md @@ -0,0 +1,325 @@ +# UX-AUDIT — GitHub Store, design-system refresh + +Branch: `feat/design-system-refresh`. Audited against `Downloads/handoff 4/` (DESIGN.md, MIGRATION.md, patterns.md, tokens.json). Code citations are file:line on this branch's working tree. No implementation here; only what is, what changes, and the risks. + +--- + +## 1. Current state inventory + +Every primary screen + the chrome that wraps them. + +### 1.1 Chrome (composeApp) + +| Element | Route / file | Current behavior | +|---|---|---| +| Nav graph | `composeApp/.../app/navigation/GithubStoreGraph.kt` | 22 sealed routes (lines 6–84). `DetailsScreen` carries `sourceHost` for Forgejo/Codeberg (line 32). | +| Bottom navigation | `app/navigation/BottomNavigation.kt:60–264` + `BottomNavigationUtils.kt:20–60` | "Liquid glass" pill rail, 5 entries: Home, Search, Apps, Profile, Tweaks (lines 22–51). Apps is Android-only — filtered out on desktop (lines 54–59). Custom shape: `CircleShape` outer + animated rounded indicator (`BottomNavigation.kt:181, 193`). Two badges supported: AppsScreen (update available), ProfileScreen (unread announcements) — `BottomNavigation.kt:242–244`. | +| Top app bars | Per-screen `TopAppBar` from M3 | No shared chrome layer; each screen owns its own bar. | +| App nav host | `app/navigation/AppNavigation.kt:87–…` | `startDestination = HomeScreen` (line 89). `onNavigateToSettings` from Home actually navigates to **Profile** (line 98), not Tweaks. | + +### 1.2 Feature screens + +| # | Route | Composable | Description (1–2 sentences) | Data it surfaces (prose vs primitive today) | +|---|---|---|---|---| +| 1 | `HomeScreen` | `feature/home/presentation/HomeRoot.kt:100` → `HomeScreen` line 165 | Staggered grid of `RepositoryCard`s (`HomeRoot.kt:427`). Filter chips for category (Trending / Hot Release / Most Popular) + topic chips + platform popup; collapsible header on scroll (`HomeRoot.kt:216–234, 279–314`). | **Prose:** owner login, repo name, full description (2-line clamp), `Released 2 days ago`, language, fork badge. **Primitives:** star/fork/download counts as `InfoChip` rows, `PlatformChip` icons, `🔥` emoji prepended when `hasWeekNotPassed` (`core/presentation/.../RepositoryCard.kt:306–308`). | +| 2 | `SearchScreen` | `feature/search/presentation/SearchRoot.kt:107` | Search field + filter chips (platform / language / sort / source: GitHub / Codeberg / Custom) + paginated grid of same `RepositoryCard`. Clipboard banner detects pasted GitHub URLs. Bottom sheets for sort + language. | Same prose-heavy `RepositoryCard` payload as Home. Source chip is text. | +| 3 | `AuthenticationScreen` | `feature/auth/presentation/AuthenticationRoot.kt:…` | Three paths in one screen: web OAuth (PKCE handoff), device flow with copy-code card, PAT paste. State machine across `AuthPath.{Backend, Direct}` + PAT fallback sheet. | Code reveal text, polling spinner (CircularWavyProgressIndicator), error chips. No identity glyph at top. | +| 4 | `DetailsScreen` | `feature/details/presentation/DetailsRoot.kt` (1200+ LOC) — composes `sections/Header.kt`, `Stats.kt`, `WhatsNew.kt`, `ReleaseChannel.kt`, `ReportIssue.kt`, `Logs.kt`, `About.kt` + components/`AppHeader`, `SmartInstallButton`, `ReleaseAssetsPicker`, `VersionPicker`, `LanguagePicker`, `ApkInspectSheet`, `LinkedRepoBanner`. | `LazyColumn` of sections. Hero (`sections/Header.kt`): avatar + repo name + owner row + meta stats. `SmartInstallButton` (548 LOC) drives state across idle/download/install/done. Release notes via `multiplatform-markdown-renderer`. Translation via `TranslationControls` + `LanguagePicker`. APK Inspect sheet for Android. | **Prose:** `Released N days ago`, "Verified build" copy on signed releases (`SmartInstallButton.kt:625–632`). Star count + download count as numeric chips. Owner + ✓ self-owned badge. **No** wax-seal glyph; "Verified" is `Icons.Filled.VerifiedUser` + a text string. APK Inspect already surfaces permission groups + min/target SDK as numerals. | +| 5 | `ProfileScreen` | `feature/profile/presentation/ProfileRoot.kt:43, 177` | LazyColumn of two sections: `profile(...)` (`components/sections/ProfileSection.kt`) + `logout(...)`. Profile section embeds the **Sponsor card** (`Options.kt:114, 251–311`). Settings entry routes to TweaksScreen. **No** settings inline — they live in Tweaks. | Username, avatar circle, bio, repos count, "Favourites / Starred / Recently viewed" rows, "What's new" + "Announcements" rows, sponsor CTA, version footer. Prose-heavy. | +| 6 | `TweaksScreen` | `feature/tweaks/presentation/TweaksRoot.kt:46, 138 (TweaksScreen)` | LazyColumn of grouped settings: Account, Appearance, Installation, Language, Network, Translation, Others, About. Sub-screens for Hidden / Skipped / Host tokens / Mirror picker. Feedback sheet. Coachmark flags persisted (`apk_inspect_coachmark_shown`, `channel_chip_coachmark_shown` — `TweaksRepositoryImpl.kt:301–305`). | Toggle rows with M3 switches, dropdown pickers, value rows with chevrons. JetBrains Mono + Inter typography mismatch with handoff Fraunces requirement. | +| 7 | `DeveloperProfileScreen` | `feature/dev-profile/presentation/DeveloperProfileRoot.kt` | Username-keyed user profile: bio, stats row (`components/StatsRow.kt`), filter+sort (`FilterSortControls.kt`), grid of `DeveloperRepoItem`. | Avatar circle, followers/following/repos numerics, repo rows with star count + language. | +| 8 | `FavouritesScreen` | `feature/favourites/presentation/FavouritesRoot.kt:60, 94` | List of locally-saved favourites. Sort rules. | `FavouriteRepositoryItem` rows — avatar + name + meta. | +| 9 | `StarredReposScreen` | `feature/starred/presentation/StarredReposRoot.kt` | User's GitHub stars (live `/user/starred`). | List of repo rows. | +| 10 | `RecentlyViewedScreen` | `feature/recently-viewed/presentation/RecentlyViewedRoot.kt` | Locally-tracked viewed repos with timestamps. | `RecentlyViewedItem` rows. | +| 11 | `AppsScreen` | `feature/apps/presentation/AppsRoot.kt` (~1500 LOC) | **Android-only.** Lists installed apps; surfaces update state; multi-flavor variant picker; per-app advanced sheet; export/import (Obtainium JSON, manual link); starred picker. | `CompactAppRow` rows with `InstalledAppIcon`, version-state badges, `StatusDotCluster` for variants, source chips. Update banner for available updates. | +| 12 | `SponsorScreen` | `feature/profile/presentation/SponsorScreen.kt:56` | Donation CTAs: GitHub Sponsors (`:97-100`), Buy Me a Coffee (`:105-109`), "Other ways" (star repo / report bugs / share — `:259-298`). | Hero icon (VolunteerActivism), 2 elevated cards, support copy. | +| 13 | `ExternalImportScreen` | `feature/apps/presentation/import/ExternalImportRoot.kt` | Obtainium JSON import wizard with bucketed proposals. | Section banner + candidate rows. | +| 14 | `MirrorPickerScreen` | `feature/tweaks/presentation/mirror/MirrorPickerRoot.kt` | Pick GitHub mirror endpoint; latency probe. | Row per mirror + ms result + radio. | +| 15 | `StarredPickerScreen` | `feature/apps/presentation/starred/StarredPickerRoot.kt` | Scan signed-in user's stars for APK-shipping repos. | Rows with checkbox + asset hints. | +| 16 | `SkippedUpdatesScreen` | `feature/tweaks/presentation/skipped/SkippedUpdatesRoot.kt` | Per-app skipped-release-tag manager. | Rows + unskip button. | +| 17 | `HiddenRepositoriesScreen` | `feature/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt` | Restore hidden repos from Home/Search long-press hide. | Rows + unhide / unhide-all. | +| 18 | `WhatsNewHistoryScreen` | `core/presentation/.../whatsnew/WhatsNewHistoryScreen.kt` | Versioned what's-new release notes (markdown). | Section headings + markdown blocks. | +| 19 | `AnnouncementsScreen` | `core/presentation/.../announcements/AnnouncementsRoot.kt` | Backend-served announcements (rate-limit notices, etc.). | List of announcement rows. | +| 20 | `HostTokensScreen` | `feature/tweaks/presentation/hosttokens/HostTokensRoot.kt` | Per-host PAT CRUD (KSafe-encrypted). | Row per host + token (masked), add dialog. | + +--- + +## 2. Gap analysis — old → new (per screen) + +Applies DESIGN.md §12 migration tables and §4 vocabulary. Every entry is concrete. + +### 2.1 RepositoryCard (used by Home + Search + most lists) +- `Icons.Outlined.StarOutline` + numeric chip (`RepositoryCard.kt:245–248`) → **StarTier** (1–5 filled stars, log buckets at 1k/10k/50k/100k per tokens.json `thresholds.stars`) + count in JetBrains Mono. Reason: scannable tier replaces 5-digit text. +- `Icons.AutoMirrored.Outlined.CallSplit` fork count chip (`:250–253`) → **drop**. Reason: DESIGN.md §12.2 ("Forks/issues numbers — mostly drop; heartbeat does the 'alive' job"). Heartbeat is the live signal. +- `Icons.Outlined.Download` download chip (`:255–260`) → **DownloadWeight** dot (radius = log10(downloads)). Reason: §4.1. +- `Icons.Outlined.Code` language chip (`:262–267`) → **drop or fold into TopicGlyph**. Reason: language is rarely the decision input on an app store row. +- `"🔥 " + formatReleasedAt(...)` (`:306–311`) → **FreshnessRing around the avatar** + flame day-count pill on hero/lead cards. Reason: §1.4 forbids emoji as data; the FreshnessRing's fractional fill *is* the answer (§4.1). +- 40dp circle avatar (`:172–175`) → **avatar inside FreshnessRing**, ring color from `tokens.json.thresholds.freshness` keyed off `releaseRecency` days. Reason: §12.1. +- `PlatformChip` text labels (`:296–297`) → **PlatformGlyph** mono silhouettes (Android robot, Windows quad, macOS apple, Linux penguin). Reason: §12.1, §4.1. Filled = supported, dashed outline = unsupported. +- `IconButton(... ContentDescription = open_in_browser)` + share button (`:335–365`) → move into long-press `RepositoryActionsBottomSheet` (which already exists). Reason: §7.4 list-row template doesn't carry browser icons; cards stay quiet. +- Bottom CTA "View details" `GithubStoreButton` (`:329–333`) → **remove**; whole-card tap already navigates. Reason: §7.3 compact-card template has no inline CTA. +- Card shape (`ExpressiveCard` — `core/presentation/components/ExpressiveCard.kt`) — currently a generic M3 `Card` wrapper → **asymmetric squircle** `radD(16,12)`. Lead cards (top of Hot list) → **wonky squircle** (constraint: see §4). Reason: §5.1, §5.2. + +### 2.2 HomeScreen layout +- `LazyVerticalStaggeredGrid(StaggeredGridCells.Adaptive(350.dp))` (`HomeRoot.kt:427–440`) → **vertical list with fixed section order** (Lead release → Hot releases → Trending → Most popular → From your stars). Reason: DESIGN.md §10.1 sets section order; staggered grid hides the rank semantic the new design relies on. +- Top tab `HomeFilterChips` for HomeCategory (TRENDING / HOT_RELEASE / MOST_POPULAR) — `components/HomeFilterChips.kt`, `LiquidGlassCategoryChips` — → **time-window filter** (Today/Week/Month/All) per §9.3 / §10.1. Reason: category becomes section, not filter. Filter is temporal. +- Topic chips row (`HomeRoot.kt:317–391`) → **keep**, but restyle: M3 `FilterChip` + `RoundedCornerShape(12.dp)` → asymmetric chip radD(11,8) (§7.2). TopicCategory icons stay; ensure they map to TopicGlyph vocabulary (§4.2) — drop any not in `tokens.json.topicGlyphs.supported`. +- Platform popup (`PlatformsPopup` — separate Composable in HomeRoot) → keep; restyle with wonky squircle dropdown + outline border (§16.7). +- Section headers — none today (sections are flat behind the category chip) → add **section header with Squiggle** (§7.5). + +### 2.3 DetailsScreen +- Header `sections/Header.kt` with left-aligned avatar + repo name + chip row → on mobile, **centered hero** (`Avatar in FreshnessRing 92px → Fraunces italic name → owner login row → StarTier → topic glyphs`) per §9.4. Desktop stays left-aligned per §8.3. +- `SmartInstallButton` (Material `Button` with text + spinner — `components/SmartInstallButton.kt`) → **wonky-squircle primary CTA** with `boxShadow: 0 4px 12px -4px primary99` (DESIGN.md §7.1). Sub-meta below button ("permissions: low · arm64 · ▮▮▮▮") becomes a row of PermDot + PlatformGlyph + SignalBars. +- "Verified build" string + `Icons.Filled.VerifiedUser` (`SmartInstallButton.kt:625-632`) → **WaxSeal card** (§7.8). Mobile: top-of-section card; desktop: rotated stamp in install-panel corner. Three states: Sealed (successT), Broken (dangerT, **only place red is used aggressively**), Open (surface). +- `Stats.kt` 2-up / 4-up stats chips → **vital signs 2×2 grid** (`RELEASED · MAINTAINED · STARS · PERMISSIONS`) per §7.7. Each tile: glyph 22px + value Fraunces italic 13px + uppercase label 9.5px. +- APK Inspect sheet (`ApkInspectSheet.kt`, ~mobile-only) → keep as `APK Inspect` full-screen sheet (§9.6). Replace permissions wall-of-text with **PermDot heat + grouped chips** color-coded inline (Dangerous red / Sensitive amber / Normal green). Min/Target/Compile SDK become big italic numerals in 3 tiles. +- `LanguagePicker` translate UI → **dropdown menu** (§16.7) — copy the existing `TranslationControls.kt` pattern. +- "What's new" tabs across the top → **inner-screen takeover** with back arrow (§12.2). Version rail on the left, notes on the right. +- `Icons.Filled.Favorite/FavoriteBorder` heart button → keep; restyle as icon-only 36×36 button. +- `LinkedRepoBanner` → keep; restyle as accent-tinted banner per §7.6. + +### 2.4 SearchScreen +- M3 `TextField` (`SearchRoot.kt` — search field) → **wonky-squircle search input** with the `tokens.json.shape.wonkySquircle.search` radii (`24px 18px 26px 20px / 18px 24px 20px 26px`). +- Source chip row (GitHub / Codeberg / Custom) — `SearchSourceUi` — → keep; restyle as filter chips (§7.2). Add `+ Add filter` dashed-border affordance for custom forges. +- `LanguageFilterBottomSheet` / `SortByBottomSheet` → **bottom sheet** (§16.1) — top-corners-only wonky squircle + drag handle. +- Recent-queries quick-chips (`SearchHistorySection.kt`) → keep; chip styling per §7.2. + +### 2.5 ProfileScreen +- M3 `TopAppBar` with title only (`ProfileRoot.kt:231–242`) → top bar with **Cookie-shape user mark** on the right (§9.2). +- `profile(...)` section list (avatar + bio + nav rows) → **hero card with gradient** for identity (`tintP → surface` 135°, patterns.md "Hero card with gradient"). User avatar inside Cookie shape (§4.3) — *not* FreshnessRing (the cookie is the user-identity moment). +- `SponsorCard` (`components/sections/Options.kt:251`) — see §6 below. MIGRATION.md flags this as **must not be re-introduced** without product approval; current code still ships it. **Decision required**. +- Logout section → confirm dialog (§16.2) with Cookie-shape glyph above title. + +### 2.6 TweaksScreen +- LazyColumn of grouped settings → keep structure; apply **List screen with sections** pattern (patterns.md). Section labels become uppercase 0.04em tracking. +- M3 switches → primary fill when on, surface2 when off (§"Form / Settings group"). +- Value rows ("Theme: Ocean Blue", "Font: System") → caption + `›` chevron. +- Coachmark popups (APK Inspect pulse, ReleaseChannel chip popup) → keep; restyle popup container per §16.7. + +### 2.7 AppsScreen (Library equivalent) +- TopAppBar + filter row + sort dropdown → **Library** layout (§9.5): top-bar title "Library" + meta line "N apps · M updates available" → update banner with VersionStack glyph → tab row [Installed | Updates · 1 | Pending] → rows. +- `CompactAppRow` (`components/CompactAppRow.kt`) → **list row** template (§7.4): `[avatar in FreshnessRing] [name + heartbeat] [version tag in Mono] [Open / Update button (wonky squircle)]`. +- `StatusDotCluster` for multi-flavor variant indicator (`components/StatusDotCluster.kt`) → stays — already a primitive-style cluster; just align colors with `tokens.json.status.freshness`. +- Update banner inside the screen → accent-tinted banner with **VersionStack glyph + accent button** (§12.1, §"Update banner"). +- "Update all" extended FAB → maybe drop; the per-row Update button + update-banner CTA already covers it. Open question. +- Variant picker dialog (`VariantPickerDialog.kt`) → confirm dialog (§16.2) with VersionStack glyph at top. + +### 2.8 AuthenticationScreen +- Web-OAuth + Device-flow + PAT-paste split → **full-screen sheet** layout (§16.5) with **CookieShape · GitHub** identity mark at top. +- Device-code reveal box (`CardDefaults.elevatedCardColors`) → mono JetBrains Mono 28–32px in primary-bordered wonky-squircle code box. +- Polling spinner (`CircularWavyProgressIndicator`) → **Heartbeat glyph** + "waiting…" caption (§16.5; reuses the maintenance-state vocabulary). +- "Use Personal Access Token" fallback → outlined button at bottom of the sheet. +- PAT bottom sheet (`ModalBottomSheet`) → bottom sheet (§16.1) with drag handle + top-corners-only wonky squircle. + +### 2.9 SponsorScreen +- `Icons.Filled.VolunteerActivism` hero (`:141`) + 2 elevated cards → **decision required** before redesign. MIGRATION.md explicitly bans donate UI without product approval. If kept, restyle hero with Cookie + Squiggle; cards become squircle compact cards. + +### 2.10 Favourites / Starred / Recently-viewed +- Item composables (`FavouriteRepositoryItem`, default starred row, `RecentlyViewedItem`) → unify into the **list row** template (§7.4). All three should reuse the same row composable; today they're three slightly-different layouts. + +### 2.11 DeveloperProfileScreen +- `ProfileInfoCard` (avatar + bio) → identity hero card (gradient `tintP → surface`) with avatar inside a soft outline circle (not Cookie — Cookie is reserved for the signed-in user). `StatsRow` (followers/following/repos) → 3-tile vital-signs row (Fraunces italic numerals). +- `DeveloperRepoItem` rows → list-row template (§7.4) with StarTier instead of numeric stars. + +### 2.12 HostTokensScreen +- Per-host PAT rows (mask + edit + delete) → list rows; PAT value in JetBrains Mono `••••••••••••` (mono is the "technical fact" voice per §3.3). Add dialog → bottom sheet. + +### 2.13 MirrorPickerScreen +- Radio + ms latency rows → list row with **SignalBars** primitive replacing the numeric ms label. Numeric ms can stay in mono as the secondary line. + +### 2.14 WhatsNewHistoryScreen +- Versioned markdown sections → **inner-screen** style (§8.4): version rail + selected version notes. Existing markdown renderer is reusable. + +### 2.15 ExternalImportScreen / StarredPickerScreen +- Pre-import summary buckets → section list with squiggle headers (`patterns.md` "List screen with sections"). Candidate rows: avatar + name + matched-host chip + checkbox. Bottom CTA: outline Cancel + primary wonky "Import N". + +--- + +## 3. Information-architecture deltas (what to keep, change, drop) + +MIGRATION.md prescribes IA changes (bottom-nav 4→3, Settings into Profile, Library single-screen). Reality at GHS: + +| Handoff prescription | GHS actual today | Recommendation | +|---|---|---| +| Bottom nav 4 → 3 tabs (Home, Library, Profile) + detached Search FAB | **5 tabs** today (Home, Search, Apps, Profile, Tweaks — `BottomNavigationUtils.kt:22–51`), Apps Android-only | Move to **4** on Android (Home, Search, Library, Profile) and **3** on desktop drawer (Home, Library, Profile — Library replaces "Apps"; Search becomes top-bar / drawer search per §8.1). **Reject** the "Search becomes FAB only" recommendation — Search-as-tab is a power-user fixture in GHS and the third-most-used surface. Keep Search visible. | +| Settings moves into Profile | **Already split:** Tweaks is the settings home (`feature/tweaks/`), Profile is identity-only (`feature/profile/CLAUDE.md`). Tweaks is currently a 5th bottom-nav tab. | **Drop Tweaks from bottom nav**; surface "Settings" as a row in Profile (already wired — `HomeRoot.onNavigateToSettings` lands on Profile, then Profile→Tweaks is one tap). The route stays. | +| Library single screen replacing "Installed / Updates / Stars" tabs | **GHS today:** Apps screen has tabs/sort but no Starred tab; Starred is a separate route reached from Profile (`StarredReposScreen`). | **Don't merge Starred into Library.** Starred is GitHub-account-bound; Apps is local-install-bound. They share row visuals but not data semantics. Library = Apps (rename) with Installed/Updates sections per §9.5 ; Stars stays under Profile. | +| Profile's "Support" replaced by "Connect" + "Business inquiries"; no donations | GHS still ships `SponsorScreen` with GitHub Sponsors + Buy Me a Coffee links | **Product call required.** MIGRATION.md is opinionated; rainxchzed is the maintainer and may want sponsorship kept. Audit flags this — see §6 risks. | +| "Translate the app" CTA banned | GHS ships **README translation** (not app translation) via `TranslationRepository` — Google / Youdao / LibreTranslate / DeepL / Microsoft. App localization is 13-locale built-in via Compose resources. | **Not applicable** — handoff is talking about *contributing translations*, not in-app content translation. README translation stays. App-locale picker in Tweaks stays. | +| Multi-script font fallback for CJK/Devanagari/Arabic/Hebrew | GHS bundles Inter (Latin+Cyrillic) and JetBrains Mono only. No Noto fallback set. 8 of 13 shipped locales use non-Latin scripts (ar, bn, hi, ja, ko, ru partially, tr partially, zh-CN). | **Action required** — see §4. Add Noto Sans family fallback via Compose `FontFamily` resolver. | +| Pre-release toggle: per-app overrides global | GHS already implements this (`InstallationManagerImpl` seeds new install from `include_pre_releases` pref; existing rows keep per-app value — `feature/tweaks/CLAUDE.md`). | **No change.** Already aligned. | +| Connect card on Profile | Not present | Add only if Sponsor card is removed (replacement narrative). | + +### 3.1 Bottom-nav delta (concrete) + +`BottomNavigationUtils.items()` currently builds: +``` +0 Home, 1 Search, 2 Apps (Android), 3 Profile, 4 Tweaks +``` +Proposed: +``` +Android: Home, Search, Library, Profile (Tweaks reachable via Profile) +Desktop: Drawer with Home, Library, Profile (Search via drawer header + ⌘K; Tweaks via Profile) +``` +That's **4 on Android** (handoff says 3 — we push back; see above) and **3 on desktop** (drawer matches §8.1 exactly). + +--- + +## 4. Cross-platform / Compose constraints + +The handoff is web-native (CSS). Several conventions don't translate cleanly to Compose Multiplatform. Each constraint listed once with every place it bites. + +### 4.1 Wonky squircle (asymmetric elliptical radii) +- **CSS does:** `border-radius: 20px 14px 22px 16px / 16px 22px 14px 20px` — four corners, each with independent horizontal + vertical radii (8 numbers total). +- **Compose can't:** `RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)` takes 4 numbers, applied as circular radius per corner. There is **no built-in elliptical radius**. +- **Mitigation:** Build a custom `Shape` (Path-based) — `AsymmetricSquircleShape(topStart: Size, topEnd: Size, bottomEnd: Size, bottomStart: Size)` that emits a `Path` with `arcTo(rect, …, sweepAngle, …)` per corner using ellipse bounds. Live in `core/presentation/shape/` (new package). +- **Where it matters:** + - All primary CTAs — `SmartInstallButton.kt` (Details), the "Update" / "Open" CTAs in `AppsRoot` (Apps), `GithubStoreButton` (`core/presentation/components/GithubStoreButton.kt`) used in Home cards and Search. + - Lead release card on Home (top of Hot list). + - Search input — `feature/search/.../SearchRoot.kt` `TextField`. + - Device-code reveal box on Authentication. + - Bottom sheet top corners — `ReleaseAssetsPicker`, `LanguageFilterBottomSheet`, `SortByBottomSheet`, `LinkAppBottomSheet`, `ImportSummarySheet`, `AdvancedAppSettingsBottomSheet`, `ApkInspectSheet`, `RepositoryActionsBottomSheet`. ModalBottomSheet's `shape` parameter accepts a custom Shape — feed the asymmetric squircle there. + - Toast / Snackbar background (§16.3) — `core/presentation/.../components/` (no shared snackbar today). + - Confirm dialog containers — `LogoutDialog`, `ClearDownloadsDialog`, `VariantPickerDialog`, the unlink-external-app dialog (`DetailsRoot.kt`), the discard-pending-install dialog. + +### 4.2 Cookie shape (9-petal organic) +- **CSS does:** SVG path import. +- **Compose:** `Shape` from `Path` — feed `tokens.json.shape.cookie.path` into `Path.parseSvgPath` (KMP support varies; use `Path` API + manual cubic-Bezier commands). Live in `core/presentation/shape/CookieShape.kt` (new). +- **Where:** brand mark at top-left of HomeScreen top-bar, user mark at top-right, active bottom-nav tab background (Android), authentication identity mark, ProfileScreen avatar tile. Four touchpoints (§5.3). + +### 4.3 Squiggle underline +- **CSS does:** inline SVG `` 36–42px × 5px. +- **Compose:** small `Canvas` with `drawPath` of the `tokens.json.shape.squiggle.path`. Component `SectionHeading(text, modifier)` in `core/presentation/components/`. +- **Where:** every section header — Home Hot/Trending/Popular, Tweaks section labels, Library Installed/Updates sections, Detail About/Permissions sections, Diagnostics card (Tweaks → Feedback). + +### 4.4 Drop shadow on primary CTAs +- **CSS does:** `box-shadow: 0 4px 12px -4px primary99`. +- **Compose:** `Modifier.shadow(elevation, shape, ambientColor, spotColor)`. Custom color shadows only on **API 28+** Android — min SDK 26 → graceful fallback on 26/27 to neutral shadow. Use `androidx.compose.ui.draw.shadow` with `ambientColor` + `spotColor` and accept that on API 26/27 it falls back. + +### 4.5 Fonts (most critical mismatch) +- **Handoff wants:** Fraunces (italic 600), Inter Tight, JetBrains Mono. +- **GHS has:** Inter (NOT Inter Tight — DESIGN.md §3.4 explicitly forbids), JetBrains Mono, **no Fraunces**. See `core/presentation/.../theme/Type.kt:21–30, 14–19` and `composeResources/font/` listing. +- **Worse:** `Type.kt:43–51` uses **JetBrains Mono for displayLarge through titleSmall** — i.e. the entire heading stack is mono. DESIGN.md §3.3 explicitly says mono is "for technical artifacts (versions, hashes, package names) so it carries weight when it appears." +- **Mitigation:** bundle Fraunces (variable + italic) + Inter Tight in `core/presentation/.../composeResources/font/`. Add Noto Sans CJK/Devanagari/Arabic fallbacks (MIGRATION.md "Multi-script type stack"). Rewrite `Type.kt` to map display/headline/title → Fraunces italic; body/label → Inter Tight; introduce a new typed `monoFontFamily` for tokens explicitly tagged as mono (versions, SHAs, package names). +- **FontTheme enum** (`core/domain/.../FontTheme.kt:6–8`) currently `SYSTEM | CUSTOM("JetBrains Mono + Inter")`. The display name lies after the refactor; pre-refactor user pref persistence keeps working but the label is stale. + +### 4.6 Heartbeat animation +- **CSS does:** CSS keyframes. +- **Compose:** `rememberInfiniteTransition` + `animateFloat`. Periods from `tokens.json.thresholds.maintenance`. Skip animation when `Settings → Accessibility → Reduce motion` is on (Android: `Settings.Global.ANIMATOR_DURATION_SCALE == 0`; desktop: respect OS pref where available). + +### 4.7 macOS-style traffic-light window chrome (desktop §8) +- **CSS does:** mock traffic lights with three colored circles. +- **Compose Desktop:** `Window` on macOS already provides native traffic lights when `undecorated = false`. Don't re-draw them — let macOS render its own. On Windows/Linux, draw chrome ourselves at 36px tall with title centered. + +### 4.8 Color shadows behind FreshnessRing accent +- The per-app accent (`accent: { c, lt, dt }` per repo) is **not currently stored** anywhere. `core/data/dto/BackendRepoResponse.kt` has no accent field. **Action:** add to backend response (open question per DESIGN.md §14 OQ #2) or derive client-side (color-thief on avatar). For now, leave the accent slot unfilled and let primitives use palette `primary` as fallback. + +--- + +## 5. Honesty audit (data we invent vs derive) + +DESIGN.md §11 forbids invented data. Walk of current code: + +| Location | Today | Verdict | Action | +|---|---|---|---| +| `RepositoryCard.kt:306–308` | `"🔥 " + formatReleasedAt(updatedAt)` when `hasWeekNotPassed(updatedAt)`. The 🔥 is conditional on a real signal (≤ 7 days). | **Derivable** — but emoji forbidden by §1.4. | Replace emoji with FreshnessRing fraction (the ring already encodes "hot"). | +| `feature/home/data/.../HomeRepositoryImpl.kt:571–604` | Local affinity-`score` computed by adding bonuses (topics, language, description keywords). Drives sort. | **Borderline.** Not displayed as a percentage; only used to order. Per DESIGN.md §14 OQ #1, this is a "local sort proxy" while backend `trendingScore` is null. | **Keep**, but never surface as text. Position (`#1`, `#2`) is the only honest display. | +| `core/data/dto/BackendRepoResponse.kt:27` | `trendingScore: Double? = null` from backend. Used only for sort key in `CachedRepositoriesDataSourceImpl.kt:207`. | **Honest** — used as opaque sort key, not displayed. | No change. Don't ever render this as a percentage. | +| `SmartInstallButton.kt:625–632` | "Verified build" string + `Icons.Filled.VerifiedUser` whenever `AttestationStatus.VERIFIED`. | **Derivable** — backed by real attestation verifier (`DetailsViewModel.kt:2315`). | Migrate copy to **WaxSeal** glyph + "Sealed" Fraunces label + sha256 fingerprint in mono. The verification result is real; the *representation* needs to align with vocabulary. | +| `Stats.kt` (Details) numeric chips (stars, forks, downloads) | Real numbers from API. | **Honest.** | Replace stars with StarTier+count, drop forks (§12.2), keep downloads as DownloadWeight dot + count. | +| `formatCount(...)` (used by `RepositoryCard`) | Pretty-print thousands. | **Honest.** | Keep; render in JetBrains Mono (technical fact per §3.3). | +| Search "trending" — no current invented signal | n/a | — | — | +| "Featured" curation — **no occurrence in codebase** (verified `grep -rn "[Ff]eatured" --include='*.kt'` returns zero hits). | n/a | — | The new Lead-release slot (§10.1) is honestly "top of the filtered Hot list" — match that. | +| "+X%" trending percentages — **no occurrence in codebase** (verified). | n/a | — | Keep that way. | + +**Net:** the only active honesty violation today is the 🔥 emoji at RepositoryCard.kt:307. The local affinity score is a sort proxy and acceptable. The "Verified build" string is honest but uses the wrong vocabulary (icon-with-label instead of WaxSeal). + +--- + +## 6. Risks + breaking changes for users + +| Risk | Impact | Mitigation | +|---|---|---| +| **Bottom-nav reduction from 5 → 4 (Android)** | Tweaks tab disappearing breaks muscle memory; users tap empty space. | One-shot coachmark on Profile→Settings row first time Profile is opened post-upgrade. Add `K_SETTINGS_RELOCATION_COACHMARK_SHOWN` to `TweaksRepositoryImpl.kt:477+` (companion object) and a `getSettingsRelocationCoachmarkShown()`/`setSettingsRelocationCoachmarkShown(Boolean)` pair to `TweaksRepository.kt`. Trigger from `ProfileRoot` first composition. | +| **Apps tab renamed to Library** | Users searching for "Apps" in onboarding/help. | Keep the route name `AppsScreen` (don't churn the nav graph). Only rename the visible label `Res.string.bottom_nav_apps_title`. Add a what's-new bullet in the upcoming-release JSON. | +| **Bottom-nav 5 → 4 → 3 path on desktop** | Desktop already filters out Apps (`BottomNavigationUtils.kt:54–59`); going to drawer is a bigger redesign than dropping a tab. | Ship drawer behind a behind-feature-flag toggle in Tweaks → Appearance → "Use drawer (experimental)" until validated. Reuse the existing `convention.cmp.application` build config; no new module. | +| **Typography change (mono headings → Fraunces italic)** | Users who chose `FontTheme.CUSTOM` ("JetBrains Mono + Inter") expect mono everywhere. Visual whiplash. | Add a new `FontTheme.STORE` variant (Fraunces + Inter Tight + JetBrains Mono) as the *new default*. Keep `CUSTOM` as a legacy choice for users who don't want the change. Migration: do **not** auto-flip persisted `CUSTOM` to `STORE`; let users opt in. | +| **Palette change (Ocean/Purple/Forest/Slate/Amber → Nord/Cream/Forest/Plum)** | `AppTheme` enum (`core/domain/.../AppTheme.kt:3–10`) loses two values, adds two. Persisted `AppTheme.fromName` returns OCEAN default — users with PURPLE/SLATE/AMBER land on OCEAN→Nord silently. | Map old→new in `AppTheme.fromName` migration: `OCEAN→NORD`, `PURPLE→PLUM`, `FOREST→FOREST`, `SLATE→NORD`, `AMBER→CREAM`. Surface a one-shot snackbar "Your theme was updated to Nord — pick a different one in Tweaks → Appearance" if the old name was not NORD. Persist a `theme_migrated_v1` flag in `TweaksRepository`. | +| **Sponsor card removal** | Maintainer revenue (rainxchzed). | **Decision required by product / maintainer.** Audit recommends *keeping* SponsorScreen at the project's discretion regardless of MIGRATION.md ban — but restyle, don't remove, unless owner says so. | +| **Sheet shape change (rounded → wonky asymmetric)** | Visual change only, no functional regression. | None needed. | +| **Heartbeat motion on Library rows** | Users with reduce-motion will get static dot per DESIGN.md §6.1; tokens.json `thresholds.maintenance` already has a `dormant` (`heartbeat_period_s: null`) fallback. | Honor system reduce-motion: snap to "dormant" period (no animation). | +| **AppsRoot complexity (~1500 LOC)** | Refactor risk: regressing variant picker, advanced sheet, import flow. | Migrate visuals only; keep all behavior + state machinery. New `CompactAppRow` is a re-skin of the existing — same params. | +| **Markdown rendering in inner-screen takeover** | Existing chunked progressive rendering + theme-aware images (`details/CLAUDE.md` "Markdown perf") is fragile. | Reuse existing markdown stack 1:1. Only the surrounding chrome changes (back arrow + tabs + version rail). | + +--- + +## 7. Open questions for product (beyond DESIGN.md §14) + +| # | Question | Conflicts with | +|---|---|---| +| 1 | **Per-host PATs (Codeberg / Forgejo / custom forges).** Handoff assumes a single GitHub identity. How do non-GitHub forges represent "the signed-in user" in the user-mark Cookie shape on top bars? Display the GitHub user even when looking at a Codeberg repo? | DESIGN.md §9.2 + §16.5 (CookieShape · GitHub identity mark) assumes one identity. | +| 2 | **Foreign-source repos (`sourceHost != null`) on Home / Search.** Codeberg / Forgejo repos are interleaved with GitHub repos in lists. Do they show the same FreshnessRing / StarTier / DownloadWeight (signal vocabulary is host-agnostic) — but with a tiny host-glyph (Forgejo flame icon, Codeberg blue G) somewhere on the card? | DESIGN.md doesn't address multi-source rows. `RepoIdCodec` packs host into `repoId`. | +| 3 | **Multi-flavor installed apps (#638).** A single repo can ship multiple APK variants (generic + Play). `StatusDotCluster` already shows this. New design has no concept of "this row represents N installed packages." How does the row's Update CTA disambiguate? | DESIGN.md §9.5 Library rows assume 1 repo = 1 install. | +| 4 | **Web-OAuth path with handoff custom-scheme.** The handoff §16.5 OAuth full-screen sheet shows a device-flow code box. Our primary path is web OAuth (no code) — the user gets bounced to a browser and back. The sheet should reflect that: "Open browser → Sign in → Return here" steps, not "Open github.com/login/device → enter code". | §16.5 numbered steps assume device flow as primary. | +| 5 | **KSafe-encrypted prefs and the Diagnostics card (§16.4).** The "what we'll attach" JSON pretty-prints prefs. Some prefs are KSafe-encrypted (per-host PATs, translation API keys). They must NEVER appear in diagnostics — even masked. Confirm the diagnostics block lists only non-secret fields, and that the Tweaks feedback sheet (`FeedbackBottomSheet`) is already aligned with this. | §16.4 doesn't enumerate which fields are secret. | +| 6 | **Shizuku / Dhizuku / Root installer state on Library rows.** When a row is installed via Shizuku, do we surface that as a glyph next to the version, or only inside the per-app advanced sheet? | DESIGN.md is GitHub-Releases-only — Android installer plurality isn't addressed. | +| 7 | **Skipped releases (per-app `skippedReleaseTag`, E542).** When a user has skipped v2.7.5 of immich, do we show VersionStack (v2.7.0 → v2.7.5) as "you skipped this" with a different color, or suppress entirely until a newer release? Current code suppresses CTA until strictly-newer release. | DESIGN.md §12.4 says VersionStack badge replaces M3 numeric "1"; doesn't address skip state. | +| 8 | **Ignore-updates** (silence the badge per app). Same family as skip — should the row still show a "Update available" VersionStack with a "silenced" dashed-outline modifier? | Not addressed. | +| 9 | **Trust-fingerprint storage** (DESIGN.md §14 OQ #4) — where do we persist the previously-installed signing fingerprint? Compose existing `AttestationVerifier` (`DetailsViewModel.kt:2315`) — is the "previously-installed fingerprint" available, or do we need a new Room column on `InstalledApp`? | OQ #4 already flagged; needs concrete answer for the broken-wax-seal flow. | +| 10 | **Translation provider credentials in the OAuth sheet treatment.** TweaksRoot's Translation section has LibreTranslate + DeepL + Microsoft credential forms with show/hide toggles. These don't map to any handoff section. Keep current pattern; flag for review. | n/a | +| 11 | **Locale picker remains in Tweaks** (13 locales) — handoff doesn't address app-level locale switching. Keep as-is. | DESIGN.md doesn't mention. | +| 12 | **Sponsor / "Connect" call.** Final answer needed before §2.9 work begins. | MIGRATION.md "Risky areas". | +| 13 | **What-to-do with "Tweaks" as a brand word.** Rename to "Settings" matching handoff vocabulary, or keep "Tweaks" (existing affordance, localized in 13 languages)? | DESIGN.md §8.1 uses "Settings" in the drawer; existing string `bottom_nav_profile_tweaks` is "Tweaks". | +| 14 | **Announcements & WhatsNew** sit inside Profile today. Handoff doesn't address them. Keep, restyle. | n/a | + +--- + +## 8. Recommended sequencing (KMP/CMP-tailored, replaces MIGRATION.md order) + +MIGRATION.md is web-bottom-up. Tuned for our layered modules: + +| # | Phase | Concrete paths | Why | +|---|---|---|---| +| 1 | **Foundation — design tokens module** | New: `core/presentation/src/commonMain/.../tokens/` with `Palette.kt` (4 palettes × light/dark from tokens.json), `Status.kt` (freshness/wax/perm/trend hex), `Shapes.kt`, `Typography.kt`, `Motion.kt`. Rewrite: `core/presentation/.../theme/Color.kt`, `Theme.kt`, `Type.kt`. | Everything below consumes tokens. No screen work yet. | +| 2 | **Fonts** | Bundle Fraunces variable+italic + Inter Tight + Noto Sans CJK/Devanagari/Arabic into `core/presentation/.../composeResources/font/`. Rewrite `Type.kt` mapping (Fraunces for display/headline/title; Inter Tight for body/label; mono for explicit-mono slots). Migrate `FontTheme` enum: add `STORE` value, leave `CUSTOM` for legacy. | Phase-1 typography unblocks every label + heading change downstream. | +| 3 | **Shape primitives (Compose-specific)** | New: `core/presentation/src/commonMain/.../shape/AsymmetricSquircleShape.kt`, `WonkySquircleShape.kt`, `CookieShape.kt`, `SquiggleUnderline.kt` (Canvas composable). | Required before any card/CTA/sheet refactor. | +| 4 | **Silent Vocabulary primitives** | New: `core/presentation/src/commonMain/.../primitives/FreshnessRing.kt`, `Heartbeat.kt`, `StarTier.kt`, `WaxSeal.kt`, `VersionDelta.kt`, `VersionStack.kt`, `PermDot.kt`, `PlatformGlyph.kt`, `TopicGlyph.kt`, `SignalBars.kt`, `DownloadWeight.kt`, `LicensePosture.kt`. All take their data via typed inputs (days since release, star count, etc.) — no string parsing. | Bottom-up: the 12 primitives compose every screen below. Drop-in replacements for existing chips. | +| 5 | **Shared chrome — TopBar, BottomNav, SectionHeader, Banner, Toast** | Rewrite: `composeApp/.../app/navigation/BottomNavigation.kt` (Cookie-shape active tab, Fraunces italic label, VersionStack badge). Rewrite: `BottomNavigationUtils.kt` (drop Tweaks from items, add Library label). New: `core/presentation/.../components/SectionHeader.kt`, `Banner.kt`, `StoreToast.kt`. | Cross-cutting chrome lands before any screen. Bottom-nav reduction lives here. | +| 6 | **RepositoryCard rewrite** | Rewrite: `core/presentation/.../components/RepositoryCard.kt` + `ExpressiveCard.kt`. Now uses primitives + asymmetric squircle. | Single component change ripples to Home + Search + Favourites + Starred + Recently-viewed + DeveloperProfile. Biggest visual win per hour. | +| 7 | **HomeScreen** | Rewrite section order (Lead → Hot → Trending → Popular → Stars). Replace `HomeCategory` chips with time-window filter (Today/Week/Month/All) inside the state. Add lead-card with wonky squircle + radial bloom. Edit: `feature/home/presentation/HomeRoot.kt`, `HomeState.kt` (add `timeWindow`), `HomeViewModel.kt`. Keep `HomeRepository` interface intact. | High-traffic; sets the tone. The IA change (category becomes section, not filter) is the headline. | +| 8 | **AppsScreen → Library** | Edit: `feature/apps/presentation/AppsRoot.kt` (new layout per §9.5), `CompactAppRow.kt` (new primitives). Rename label `bottom_nav_apps_title` → `bottom_nav_library_title` (touch every 13 locales). Route name stays `AppsScreen` to avoid churn. | Establishes the list-row template (§7.4) used everywhere else. | +| 9 | **ProfileScreen** | Edit: `feature/profile/presentation/ProfileRoot.kt`, `components/sections/ProfileSection.kt`, `Options.kt`. Cookie-shape user mark in top bar. Identity hero card with gradient. Settings row → Tweaks (already wired). **Decide** sponsor card. | Required before bottom-nav reduction. | +| 10 | **Bottom-nav reduction** | Edit: `BottomNavigationUtils.kt:20–60` — drop Tweaks entry. Add settings-relocation coachmark in `TweaksRepository` + trigger from `ProfileRoot`. | Do AFTER Profile lands. Don't strand users. | +| 11 | **DetailsScreen** | Edit: `feature/details/presentation/DetailsRoot.kt`, `sections/Header.kt`, `Stats.kt`, `components/SmartInstallButton.kt`. WaxSeal card. Vital signs 2×2. Wonky-squircle CTA. Permission heat in `ApkInspectSheet.kt`. About / What's new as inner-screen takeover. | Longest screen; touches the most components. After bottom-up primitives stabilize. | +| 12 | **SearchScreen** | Edit: `feature/search/presentation/SearchRoot.kt` — wonky-squircle search input, restyled chips, restyled sheets. | Reuses primitives; quick. | +| 13 | **Tweaks** | Edit: `feature/tweaks/presentation/TweaksRoot.kt`, `components/sections/*.kt`. Section labels with Squiggle. Form rows with tinted icon shell + chevron. Feedback diagnostics card per §16.4. | Low priority — users hit Tweaks rarely. | +| 14 | **AuthenticationScreen** | Edit: `feature/auth/presentation/AuthenticationRoot.kt`. Full-screen sheet layout. Cookie · GitHub identity mark. Heartbeat polling indicator. Code-box wonky squircle. Adapt step copy for web OAuth path (Q #4 above). | Done late; rarely-seen post-signup. | +| 15 | **Net-new screens** | DeveloperProfile re-skin, Favourites/Starred/Recently-viewed unify to list-row template, HostTokens / Skipped / Hidden / Mirror / WhatsNew / Announcements re-skin. | All reuse phase-4–6 primitives. Mostly parallelizable. | +| 16 | **Sponsor decision** | Either remove `SponsorScreen.kt` + nav entry, or restyle. Coordinate with maintainer. | Last — depends on product call (Q #12). | +| 17 | **Polish** | Heartbeat reduce-motion guard. Theme-migration mapping (Q under §6 "Palette change"). Localization audit (Fraunces glyph coverage for ar / bn / hi). | Catch-up; ship behind the last bullet. | + +### 8.1 Per-phase exit criteria +- After Phase 4: a primitives gallery preview composable renders all 12 silent vocab items + 2 expressive shapes in a dev-only screen behind a debug flag. +- After Phase 6: Home + Search + Favourites + Starred render with new RepositoryCard and no other screen yet touched. Visual regression expected, no functional regression. +- After Phase 11: every primary user flow (discover → install → manage → settings) reads in the new vocabulary; remaining work is net-new screens + polish. + +### 8.2 Parallelization hints +- Phases 1–4 are strictly sequential (foundation). +- Phases 5–6 can fork once Phase 4 is on `main`. +- Phases 7–13 can fork once Phase 6 lands. +- Phase 14 (Auth) and Phase 15 (net-new) parallelize freely. +- One screen per session per MIGRATION.md principle 1 — still holds. + +--- + +## 9. Out-of-scope notes (flagged, not actioned) + +- `HomeRepositoryImpl.kt:571–604` local affinity score: stay as a sort-only proxy, never displayed. If backend supplies real `trendingScore` in a future API revision, this code can be deleted (open question DESIGN.md §14 OQ #1). +- `core/domain/.../util/EmojiShortcodes.kt` (290+ short-code → emoji map) is for **user content** (release notes, READMEs) only. Don't touch. +- `WhatsNewLoaderImpl.kt` per-version markdown is content, not chrome. Re-skin the renderer wrapper, not the markdown. +- `core/data/network/BackendApiClient.kt` calls out to `api.github-store.org` — unchanged by design refresh. +- Per-host PATs (`HostTokenRepository`, `HostTokensScreen`) — visual re-skin only; AES-256-GCM storage stays. + +--- + +End of audit. Path: `/Users/rainxchzed/Documents/development/kmp/GitHub-Store/.design/UX-AUDIT.md`. From d001b6596489848ff756efaa3e11598a8225c94a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 09:57:05 +0500 Subject: [PATCH 002/172] refactor: drop SponsorScreen, prep nav for redesign --- CLAUDE.md | 2 +- .../app/navigation/AppNavigation.kt | 12 - .../app/navigation/BottomNavigationUtils.kt | 6 - .../app/navigation/GithubStoreGraph.kt | 2 +- .../composeResources/values-ar/strings-ar.xml | 25 -- .../composeResources/values-bn/strings-bn.xml | 25 -- .../composeResources/values-es/strings-es.xml | 25 -- .../composeResources/values-fr/strings-fr.xml | 25 -- .../composeResources/values-hi/strings-hi.xml | 25 -- .../composeResources/values-it/strings-it.xml | 25 -- .../composeResources/values-ja/strings-ja.xml | 25 -- .../composeResources/values-ko/strings-ko.xml | 25 -- .../composeResources/values-pl/strings-pl.xml | 25 -- .../composeResources/values-ru/strings-ru.xml | 25 -- .../composeResources/values-tr/strings-tr.xml | 25 -- .../values-zh-rCN/strings-zh-rCN.xml | 25 -- .../composeResources/values/strings.xml | 25 -- feature/profile/CLAUDE.md | 6 +- .../profile/presentation/ProfileAction.kt | 2 - .../profile/presentation/ProfileRoot.kt | 5 - .../profile/presentation/ProfileViewModel.kt | 4 - .../profile/presentation/SponsorScreen.kt | 356 ------------------ .../components/sections/Options.kt | 83 ---- 23 files changed, 5 insertions(+), 798 deletions(-) delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/SponsorScreen.kt diff --git a/CLAUDE.md b/CLAUDE.md index 78e14f33a..6929b9d15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ class XViewModel : ViewModel() { ### Navigation -`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `SponsorScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path. +`@Serializable` sealed interface `GithubStoreGraph` in `composeApp/.../app/navigation/`. Routes: `HomeScreen`, `SearchScreen`, `AuthenticationScreen`, `ProfileScreen`, `TweaksScreen`, `FavouritesScreen`, `StarredReposScreen`, `RecentlyViewedScreen`, `AppsScreen`, `OnboardingScreen`, `ExternalImportScreen`, `MirrorPickerScreen`, `StarredPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `WhatsNewHistoryScreen`, `AnnouncementsScreen`, `HostTokensScreen`, `DetailsScreen(repositoryId, owner, repo, isComingFromUpdate, sourceHost)`, `DeveloperProfileScreen(username)`. `DetailsScreen.sourceHost` is non-null for Codeberg / Forgejo / custom-forge repos — routes all `DetailsRepository` calls through `ForgejoClientRegistry` instead of the GitHub-backed default path. ### DI diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 0afd9742f..8363ba8ba 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -43,7 +43,6 @@ import zed.rainxch.githubstore.app.announcements.AnnouncementsViewModel import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel import zed.rainxch.home.presentation.HomeRoot import zed.rainxch.profile.presentation.ProfileRoot -import zed.rainxch.profile.presentation.SponsorScreen import zed.rainxch.recentlyviewed.presentation.RecentlyViewedRoot import zed.rainxch.search.presentation.SearchRoot import zed.rainxch.search.presentation.mappers.toSearchPlatformUi @@ -312,9 +311,6 @@ fun AppNavigation( onNavigateToDevProfile = { username -> navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username)) }, - onNavigateToSponsor = { - navController.navigate(GithubStoreGraph.SponsorScreen) - }, onNavigateToWhatsNew = { navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) }, @@ -355,14 +351,6 @@ fun AppNavigation( ) } - composable { - SponsorScreen( - onNavigateBack = { - navController.navigateUp() - }, - ) - } - composable { MirrorPickerRoot( onNavigateBack = { navController.popBackStack() }, diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt index 09e959bb5..c5c1aecae 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt @@ -43,12 +43,6 @@ object BottomNavigationUtils { iconFilled = Icons.Filled.Person2, screen = GithubStoreGraph.ProfileScreen, ), - BottomNavigationItem( - titleRes = Res.string.bottom_nav_profile_tweaks, - iconOutlined = Icons.Outlined.Settings, - iconFilled = Icons.Filled.Settings, - screen = GithubStoreGraph.TweaksScreen, - ), ) fun allowedScreens(): List = diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index b284a3a0b..e2d079cdd 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -56,7 +56,7 @@ sealed interface GithubStoreGraph { data object AppsScreen : GithubStoreGraph @Serializable - data object SponsorScreen : GithubStoreGraph + data object OnboardingScreen : GithubStoreGraph @Serializable data object ExternalImportScreen : GithubStoreGraph diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index de76ec3c0..1c029620d 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -511,44 +511,19 @@ الحجم الحالي: مسح - ادعم GitHub Store - ادعم المشروع - مجاني للأبد.\nبدعمكم يستمر. - دعمكم يبقي GitHub Store مجانيًا ومستقلًا ومُحدّثًا باستمرار — بدون إعلانات، بدون تتبع، فقط طريقة أفضل لتثبيت التطبيقات مفتوحة المصدر. - أقوم ببناء وصيانة هذا المشروع بمفردي أثناء إنهاء دراستي. كل مساهمة — كبيرة أو صغيرة — تذهب مباشرة لتغطية تكاليف الخوادم والتطوير وإبقاء التطبيق مجانيًا بدون إعلانات للجميع. - صوّت لـ GitHub Store! - تم ترشيح GitHub Store لجوائز Golden Kodee في KotlinConf 2026. - 1. التسجيل - 2. التصويت - ينتهي التصويت في 22 مارس - 1. سجل في منصة الجوائز (المتابعة عبر Google) - 2. اضغط على زر التصويت أدناه - 3. ابحث عن Usmon Narzullayev واضغط تصويت - GitHub Sponsors - شهري أو لمرة واحدة — يموّل التطوير مباشرة - Buy Me a Coffee - مساهمة سريعة لمرة واحدة - طرق أخرى للمساعدة - ضع نجمة للمستودع - يزيد الظهور ويساعد الآخرين على اكتشافه - الإبلاغ عن الأخطاء - كل بلاغ يجعل التطبيق أفضل للجميع - شارك مع الأصدقاء - الكلمة المنقولة هي أفضل تسويق - كل مساهمة — مالية أو غير ذلك — تُحدث فرقًا حقيقيًا. شكرًا لكونكم جزءًا من هذا. التثبيت diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index c50a05434..3c1a044ce 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -510,44 +510,19 @@ বর্তমান আকার: পরিষ্কার করুন - GitHub Store সমর্থন করুন - প্রকল্পকে সমর্থন করুন - চিরকাল বিনামূল্যে।\nআপনাদের সহায়তায় চলে। - আপনার সমর্থন GitHub Store কে বিনামূল্যে, স্বাধীন এবং সক্রিয়ভাবে রক্ষণাবেক্ষণ করে রাখে — কোনো বিজ্ঞাপন নেই, কোনো ট্র্যাকিং নেই, শুধু ওপেন-সোর্স অ্যাপ ইনস্টল করার একটি ভালো উপায়। - আমি পড়াশোনা শেষ করতে করতে একাই এই প্রকল্পটি তৈরি ও রক্ষণাবেক্ষণ করছি। প্রতিটি অবদান — বড় হোক বা ছোট — সরাসরি সার্ভার খরচ, উন্নয়ন এবং সবার জন্য অ্যাপটিকে বিজ্ঞাপনমুক্ত রাখতে ব্যবহৃত হয়। - GitHub Store এর জন্য ভোট দিন! - GitHub Store KotlinConf 2026 এর Golden Kodee Awards এর জন্য মনোনীত হয়েছে। - 1. নিবন্ধন করুন - 2. ভোট দিন - ভোট ২২ মার্চ পর্যন্ত - 1. পুরস্কার প্ল্যাটফর্মে নিবন্ধন করুন (Google দিয়ে চালিয়ে যান) - 2. নিচে Vote চাপুন - 3. Usmon Narzullayev খুঁজে Vote চাপুন - GitHub Sponsors - মাসিক বা একবারের — সরাসরি উন্নয়নে অবদান রাখে - Buy Me a Coffee - দ্রুত একবারের অবদান - সহায়তার অন্যান্য উপায় - রিপোজিটরিতে স্টার দিন - দৃশ্যমানতা বাড়ায় এবং অন্যদের খুঁজে পেতে সাহায্য করে - বাগ রিপোর্ট করুন - প্রতিটি রিপোর্ট সবার জন্য অ্যাপটিকে আরও ভালো করে - বন্ধুদের সাথে শেয়ার করুন - মুখের কথাই সেরা মার্কেটিং - প্রতিটি অবদান — আর্থিক হোক বা না হোক — সত্যিই একটি পার্থক্য তৈরি করে। এর অংশ হওয়ার জন্য আপনাকে ধন্যবাদ। ইনস্টলেশন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index b0c330a70..384a0832e 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -475,40 +475,15 @@ Tamaño actual: Borrar - Apoya GitHub Store - Apoyar el proyecto - Gratis para siempre.\nFinanciado por ti. - Tu apoyo mantiene GitHub Store gratuito, independiente y activamente mantenido — sin anuncios, sin rastreo, solo una mejor forma de instalar apps de código abierto. - Desarrollo y mantengo este proyecto solo mientras termino mis estudios. Cada contribución — grande o pequeña — va directamente a costos de servidor, desarrollo y mantener la app sin anuncios para todos. - ¡Vota por GitHub Store! - GitHub Store está nominado a los Golden Kodee Awards en KotlinConf 2026. Tu voto toma solo 2 minutos y significa mucho. - 1. Registrarse - 2. Votar - La votación cierra el 22 de marzo - 1. Regístrate en la plataforma de premios (Continuar con Google) - 2. Toca Votar abajo para abrir la página - 3. Busca a Usmon Narzullayev y pulsa Votar - GitHub Sponsors - Mensual o puntual — financia directamente el desarrollo - Buy Me a Coffee - Contribución rápida de una sola vez - OTRAS FORMAS DE AYUDAR - Dar estrella al repositorio - Aumenta la visibilidad y ayuda a otros a encontrarlo - Reportar errores - Cada reporte mejora la app para todos - Compartir con amigos - El boca a boca es el mejor marketing - Cada contribución — financiera o no — marca una diferencia real. Gracias por ser parte de esto. Instalación diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index d3f8a6402..058d77e26 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -475,41 +475,16 @@ Taille actuelle : Vider - Soutenir GitHub Store - Soutenir le projet - Gratuit pour toujours.\nFinancé par vous. - Votre soutien permet de garder GitHub Store gratuit, indépendant et activement maintenu — sans publicité, sans suivi, juste une meilleure façon d’installer des apps open-source. - Je développe et maintiens ce projet seul tout en finissant mes études. Chaque contribution — grande ou petite — va directement aux coûts de serveur, au développement et au maintien de l’app sans publicité pour tous. - Votez pour GitHub Store ! - GitHub Store est nommé aux Golden Kodee Awards de KotlinConf 2026. Votre vote prend seulement 2 minutes. - 1. S’inscrire - 2. Voter - Vote jusqu’au 22 mars - 1. Inscrivez-vous sur la plateforme (Continuer avec Google) - 2. Appuyez sur Voter ci-dessous - 3. Trouvez Usmon Narzullayev et cliquez sur Voter - GitHub Sponsors - Mensuel ou ponctuel — finance directement le développement - Buy Me a Coffee - Contribution rapide en une fois - AUTRES FAÇONS D’AIDER - Mettre une étoile au dépôt - Augmente la visibilité et aide les autres à le trouver - Signaler des bugs - Chaque signalement améliore l’app pour tous - Partager avec des amis - Le bouche-à-oreille est le meilleur marketing - Chaque contribution — financière ou non — fait une vraie différence. Merci d'en faire partie. Installation diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 9f31f1e7d..f1f043297 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -509,44 +509,19 @@ वर्तमान आकार: साफ़ करें - GitHub Store का समर्थन करें - प्रोजेक्ट को समर्थन दें - हमेशा मुफ्त।\nआपके सहयोग से चलता है। - आपका सहयोग GitHub Store को मुफ्त, स्वतंत्र और सक्रिय रूप से अपडेट रखता है — कोई विज्ञापन नहीं, कोई ट्रैकिंग नहीं, बस ओपन-सोर्स ऐप्स इंस्टॉल करने का एक बेहतर तरीका। - मैं पढ़ाई पूरी करते हुए अकेले इस प्रोजेक्ट को बनाता और संभालता हूँ। हर योगदान — बड़ा हो या छोटा — सीधे सर्वर लागत, विकास और सबके लिए ऐप को विज्ञापन-मुक्त रखने में जाता है। - GitHub Store के लिए वोट करें! - GitHub Store KotlinConf 2026 Golden Kodee Awards के लिए नामांकित है। - 1. रजिस्टर करें - 2. वोट करें - वोटिंग 22 मार्च को बंद होगी - 1. प्लेटफॉर्म पर रजिस्टर करें (Google से जारी रखें) - 2. नीचे वोट बटन दबाएँ - 3. Usmon Narzullayev खोजें और वोट करें - GitHub Sponsors - मासिक या एक बार — सीधे विकास को वित्तपोषित करता है - Buy Me a Coffee - त्वरित एक बार का योगदान - मदद करने के अन्य तरीके - रिपॉजिटरी को स्टार दें - दृश्यता बढ़ाता है और दूसरों को इसे खोजने में मदद करता है - बग रिपोर्ट करें - हर रिपोर्ट सबके लिए ऐप को बेहतर बनाती है - दोस्तों के साथ साझा करें - मुँह की बात सबसे अच्छा मार्केटिंग है - हर योगदान — आर्थिक हो या न हो — एक वास्तविक फर्क डालता है। इसका हिस्सा होने के लिए धन्यवाद। इंस्टॉलेशन diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 8b4c75a31..949679c5d 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -511,42 +511,17 @@ Dimensione attuale: Pulisci - Supporta GitHub Store - Supporta il progetto - Gratuito per sempre.\nFinanziato da te. - Il tuo supporto mantiene GitHub Store gratuito, indipendente e attivamente aggiornato — niente pubblicità, niente tracciamento, solo un modo migliore per installare app open-source. - Sviluppo e mantengo questo progetto da solo mentre finisco gli studi. Ogni contributo — grande o piccolo — va direttamente ai costi del server, allo sviluppo e a mantenere l'app senza pubblicità per tutti. - Vota GitHub Store! - GitHub Store è nominato ai Golden Kodee Awards al KotlinConf 2026. - 1. Registrati - 2. Vota - Le votazioni chiudono il 22 marzo - 1. Registrati sulla piattaforma (Continua con Google) - 2. Tocca Vota qui sotto - 3. Trova Usmon Narzullayev e premi Vota - GitHub Sponsors - Mensile o una tantum — finanzia direttamente lo sviluppo - Buy Me a Coffee - Contributo rapido una tantum - ALTRI MODI PER AIUTARE - Metti una stella al repository - Aumenta la visibilità e aiuta gli altri a trovarlo - Segnala bug - Ogni segnalazione migliora l'app per tutti - Condividi con amici - Il passaparola è il miglior marketing - Ogni contributo — finanziario o meno — fa davvero la differenza. Grazie per farne parte. diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index aedd1b79e..c3238263f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -475,42 +475,17 @@ 現在のサイズ: クリア - GitHub Store を支援 - プロジェクトを支援 - ずっと無料。\nあなたの支援で成り立っています。 - あなたの支援が GitHub Store を無料で、独立して、積極的にメンテナンスされた状態に保ちます — 広告なし、追跡なし、オープンソースアプリをインストールするためのより良い方法です。 - 学業を終えながら、このプロジェクトを一人で開発・維持しています。すべての貢献 — 大きくても小さくても — サーバー費用、開発、そしてアプリを全員に広告なしで提供し続けることに直接使われます。 - GitHub Store に投票! - KotlinConf 2026 の Golden Kodee Awards にノミネートされています。投票は2分で完了します。 - 1. 登録 - 2. 投票 - 投票締切:3月22日 - 1. 賞のプラットフォームに登録(Googleで続行) - 2. 下の「投票」をタップ - 3. Usmon Narzullayev を見つけて投票 - GitHub Sponsors - 月額または一回 — 開発を直接支援 - Buy Me a Coffee - 手軽な一回限りの貢献 - 他の支援方法 - リポジトリにスター - 知名度を上げ、他の人が見つけやすくなります - バグを報告 - すべての報告がアプリをみんなのために改善します - 友達と共有 - 口コミが最高のマーケティングです - すべての貢献 — 金銭的であろうとなかろうと — 本当に大きな力になります。参加してくれてありがとうございます。 インストール diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 54e372517..535e0981b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -508,43 +508,18 @@ 현재 크기: 지우기 - GitHub Store 지원 - 프로젝트 지원하기 - 영원히 무료.\n여러분이 후원합니다. - 여러분의 후원이 GitHub Store를 무료로, 독립적으로, 활발히 유지보수되도록 합니다 — 광고 없음, 추적 없음, 오픈소스 앱을 설치하는 더 나은 방법입니다. - 학업을 마치면서 이 프로젝트를 혼자 개발하고 유지하고 있습니다. 모든 기여 — 크든 작든 — 서버 비용, 개발, 그리고 모두를 위해 앱을 광고 없이 유지하는 데 직접 사용됩니다. - GitHub Store에 투표하세요! - GitHub Store가 KotlinConf 2026 Golden Kodee Awards 후보에 올랐습니다. - 1. 등록 - 2. 투표 - 투표 마감: 3월 22일 - 1. 플랫폼에 등록 (Google로 계속) - 2. 아래에서 투표 버튼 누르기 - 3. Usmon Narzullayev를 찾아 투표 클릭 - GitHub Sponsors - 월간 또는 일회성 — 개발을 직접 후원 - Buy Me a Coffee - 간편한 일회성 기여 - 다른 도움 방법 - 저장소에 스타 주기 - 가시성을 높이고 다른 사람들이 찾을 수 있도록 도움 - 버그 신고 - 모든 신고가 모두를 위해 앱을 개선합니다 - 친구와 공유 - 입소문이 최고의 마케팅입니다 - 모든 기여 — 금전적이든 아니든 — 진정한 변화를 만듭니다. 함께해 주셔서 감사합니다. diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 4d7e789e0..be76131ef 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -473,44 +473,19 @@ Aktualny rozmiar: Wyczyść - Wesprzyj GitHub Store - Wesprzyj projekt - Darmowy na zawsze.\nFinansowany przez Ciebie. - Twoje wsparcie utrzymuje GitHub Store darmowym, niezależnym i aktywnie rozwijanym — bez reklam, bez śledzenia, po prostu lepszy sposób na instalowanie aplikacji open-source. - Tworzę i utrzymuję ten projekt samodzielnie, kończąc jednocześnie szkołę. Każdy wkład — duży czy mały — idzie bezpośrednio na koszty serwera, rozwój i utrzymanie aplikacji bez reklam dla wszystkich. - Głosuj na GitHub Store! - GitHub Store został nominowany do Golden Kodee Awards na KotlinConf 2026. - 1. Zarejestruj się - 2. Głosuj - Głosowanie kończy się 22 marca - 1. Zarejestruj się na platformie (Kontynuuj z Google) - 2. Kliknij Głosuj poniżej - 3. Znajdź Usmon Narzullayev i kliknij Głosuj - GitHub Sponsors - Miesięczne lub jednorazowe — bezpośrednio finansuje rozwój - Buy Me a Coffee - Szybki jednorazowy wkład - INNE SPOSOBY POMOCY - Dodaj gwiazdkę repozytorium - Zwiększa widoczność i pomaga innym go znaleźć - Zgłoś błąd - Każdy raport czyni aplikację lepszą dla wszystkich - Udostępnij znajomym - Polecenie to najlepszy marketing - Każdy wkład — finansowy lub nie — naprawdę robi różnicę. Dziękuję, że jesteś częścią tego. diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index d38a3a4af..40b594071 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -475,42 +475,17 @@ Текущий размер: Очистить - Поддержать GitHub Store - Поддержать проект - Бесплатно навсегда.\nБлагодаря вам. - Ваша поддержка позволяет GitHub Store оставаться бесплатным, независимым и активно развиваемым — без рекламы, без отслеживания, просто лучший способ устанавливать open-source приложения. - Я разрабатываю и поддерживаю этот проект в одиночку, заканчивая учёбу. Каждый вклад — большой или маленький — идёт напрямую на серверные расходы, разработку и поддержание приложения без рекламы для всех. - Голосуйте за GitHub Store! - GitHub Store номинирован на Golden Kodee Awards на KotlinConf 2026. - 1. Зарегистрироваться - 2. Проголосовать - Голосование до 22 марта - 1. Зарегистрируйтесь на платформе (Войти через Google) - 2. Нажмите «Голосовать» ниже - 3. Найдите Usmon Narzullayev и нажмите «Голосовать» - GitHub Sponsors - Ежемесячно или разово — напрямую финансирует разработку - Buy Me a Coffee - Быстрый разовый вклад - ДРУГИЕ СПОСОБЫ ПОМОЧЬ - Поставить звезду репозиторию - Повышает видимость и помогает другим его найти - Сообщить об ошибке - Каждый отчёт делает приложение лучше для всех - Поделиться с друзьями - Сарафанное радио — лучший маркетинг - Каждый вклад — финансовый или нет — действительно имеет значение. Спасибо, что вы с нами. diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 3fb7b6fa5..b6d555731 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -507,44 +507,19 @@ Geçerli boyut: Temizle - GitHub Store'u Destekle - Projeyi destekle - Sonsuza dek ücretsiz.\nSizin desteğinizle. - Desteğiniz GitHub Store'u ücretsiz, bağımsız ve aktif olarak güncel tutar — reklam yok, izleme yok, sadece açık kaynak uygulamaları yüklemenin daha iyi bir yolu. - Okulu bitirirken bu projeyi tek başıma geliştiriyor ve sürdürüyorum. Her katkı — büyük ya da küçük — doğrudan sunucu maliyetlerine, geliştirmeye ve uygulamayı herkes için reklamsız tutmaya gidiyor. - GitHub Store için oy ver! - GitHub Store KotlinConf 2026 Golden Kodee Awards için aday gösterildi. - 1. Kayıt ol - 2. Oy ver - Oylama 22 Mart'ta kapanıyor - 1. Platformda kayıt ol (Google ile devam et) - 2. Aşağıdan Oy Ver'e dokun - 3. Usmon Narzullayev'i bul ve Oy Ver'e tıkla - GitHub Sponsors - Aylık veya tek seferlik — doğrudan geliştirmeyi finanse eder - Buy Me a Coffee - Hızlı tek seferlik katkı - YARDIM ETMENİN DİĞER YOLLARI - Depoya yıldız ver - Görünürlüğü artırır ve başkalarının bulmasına yardımcı olur - Hata bildir - Her bildirim uygulamayı herkes için daha iyi yapar - Arkadaşlarınla paylaş - Ağızdan ağıza en iyi pazarlamadır - Her katkı — finansal olsun ya da olmasın — gerçek bir fark yaratır. Bunun parçası olduğunuz için teşekkürler. diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 579d1d41c..97e3872e1 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -476,42 +476,17 @@ 当前大小: 清除 - 支持 GitHub Store - 支持项目 - 永久免费。\n由你资助。 - 你的支持让 GitHub Store 保持免费、独立且持续更新——无广告、无跟踪,只是一种更好的方式来安装开源应用。 - 我在完成学业的同时独自开发和维护这个项目。每一份贡献——无论大小——都直接用于服务器成本、开发以及让应用对所有人保持免费无广告。 - 为 GitHub Store 投票! - GitHub Store 已被提名为 KotlinConf 2026 Golden Kodee Awards。 - 1. 注册 - 2. 投票 - 投票截止:3月22日 - 1. 在奖项平台注册(使用 Google 登录) - 2. 点击下方“投票” - 3. 找到 Usmon Narzullayev 并点击投票 - GitHub Sponsors - 按月或一次性——直接资助开发 - Buy Me a Coffee - 快速一次性贡献 - 其他帮助方式 - 为仓库加星 - 提升曝光度,帮助更多人发现它 - 报告问题 - 每一个反馈都让应用对所有人更好 - 分享给朋友 - 口碑是最好的推广 - 每一份贡献——无论是否金钱——都带来真正的改变。感谢你成为其中的一员。 安装 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 21dea498c..b68abea24 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -604,42 +604,17 @@ Clear The cache is gradually cleared. - Support GitHub Store - Support the project - Free forever.\nFunded by you. - Your support keeps GitHub Store free, independent, and actively maintained — no ads, no tracking, just a better way to install open-source apps. - I build and maintain this project solo while finishing school. Every contribution — big or small — goes directly toward server costs, development, and keeping the app ad-free for everyone. - Vote for GitHub Store! - GitHub Store has been nominated for the Golden Kodee Awards at KotlinConf 2026. - 1. Register - 2. Vote - Voting until March 22 - 1. Register on the platform (Sign in with Google) - 2. Click “Vote” below - 3. Find Usmon Narzullayev and click “Vote” - GitHub Sponsors - Monthly or one-time — directly funds development - Buy Me a Coffee - Quick one-time contribution - OTHER WAYS TO HELP - Star the repository - Boosts visibility and helps others find it - Report a bug - Every report makes the app better for everyone - Share with friends - Word of mouth is the best marketing - Every contribution — financial or not — makes a real difference. Thank you for being part of this. Installation diff --git a/feature/profile/CLAUDE.md b/feature/profile/CLAUDE.md index 7a4c53643..5d22c2e66 100644 --- a/feature/profile/CLAUDE.md +++ b/feature/profile/CLAUDE.md @@ -1,6 +1,6 @@ # Profile Feature -Account-level — GitHub user profile, login/logout, sponsor entry, version info. **Settings live in `feature/tweaks/`** — this module narrow on purpose. Owns account identity + exposes `ProfileRepository.getUser()` to other features (home/search/details/starred/favourites/tweaks/dev-profile consume it for E20 self-owned badge and account-aware flows). +Account-level — GitHub user profile, login/logout, version info. **Settings live in `feature/tweaks/`** — this module narrow on purpose. Owns account identity + exposes `ProfileRepository.getUser()` to other features (home/search/details/starred/favourites/tweaks/dev-profile consume it for E20 self-owned badge and account-aware flows). ## Structure @@ -9,7 +9,7 @@ feature/profile/ ├── domain/ # ProfileRepository, UserProfile ├── data/ # ProfileRepositoryImpl, UserProfileMappers └── presentation/ - ├── ProfileViewModel / State / Action / Event / Root, SponsorScreen + ├── ProfileViewModel / State / Action / Event / Root ├── model/ProxyType (legacy — proxy lives in tweaks now) └── components/ LogoutDialog, SectionText, sections/{Account, AccountSection, ProfileSection} ``` @@ -29,7 +29,7 @@ interface ProfileRepository { ## Navigation -`GithubStoreGraph.ProfileScreen`, `SponsorScreen`. +`GithubStoreGraph.ProfileScreen`. ## Notes diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt index eee8d4ffd..3485fdf72 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt @@ -16,8 +16,6 @@ sealed interface ProfileAction { data object OnRecentlyViewedClick : ProfileAction - data object OnSponsorClick : ProfileAction - data object OnWhatsNewClick : ProfileAction data object OnWhatsNewLongClick : ProfileAction diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 9639b6034..54a47b646 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -47,7 +47,6 @@ fun ProfileRoot( onNavigateToStarredRepos: () -> Unit, onNavigateToFavouriteRepos: () -> Unit, onNavigateToRecentlyViewed: () -> Unit, - onNavigateToSponsor: () -> Unit, onNavigateToWhatsNew: () -> Unit, onPreviewWhatsNewSheet: () -> Unit, onNavigateToAnnouncements: () -> Unit, @@ -132,10 +131,6 @@ fun ProfileRoot( onNavigateToRecentlyViewed() } - ProfileAction.OnSponsorClick -> { - onNavigateToSponsor() - } - ProfileAction.OnWhatsNewClick -> { onNavigateToWhatsNew() } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 34f2a1062..c664a0db8 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -129,10 +129,6 @@ class ProfileViewModel( // Handed in composable } - ProfileAction.OnSponsorClick -> { - // Handed in composable - } - ProfileAction.OnRecentlyViewedClick -> { // Handed in composable } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/SponsorScreen.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/SponsorScreen.kt deleted file mode 100644 index 9810d6c02..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/SponsorScreen.kt +++ /dev/null @@ -1,356 +0,0 @@ -package zed.rainxch.profile.presentation - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.Coffee -import androidx.compose.material.icons.filled.EmojiEvents -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.IosShare -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.VolunteerActivism -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun SponsorScreen(onNavigateBack: () -> Unit) { - val uriHandler = LocalUriHandler.current - val onOpenUrl: (String) -> Unit = { url -> - runCatching { uriHandler.openUri(url) } - } - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(Res.string.sponsor_title), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.navigate_back), - ) - } - }, - ) - }, - containerColor = MaterialTheme.colorScheme.background, - ) { innerPadding -> - Column( - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - HeroSection() - - SponsorOptionCard( - icon = Icons.Filled.Favorite, - title = stringResource(Res.string.sponsor_github_sponsors), - description = stringResource(Res.string.sponsor_github_sponsors_desc), - onClick = { - onOpenUrl("https://github.com/sponsors/rainxchzed") - }, - ) - - SponsorOptionCard( - icon = Icons.Filled.Coffee, - title = stringResource(Res.string.sponsor_buy_me_coffee), - description = stringResource(Res.string.sponsor_buy_me_coffee_desc), - onClick = { - onOpenUrl("https://buymeacoffee.com/rainxchzed") - }, - ) - - Spacer(Modifier.height(8.dp)) - - OtherWaysSection(onOpenUrl = onOpenUrl) - - Text( - text = stringResource(Res.string.sponsor_thank_you), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) - - Spacer(Modifier.height(16.dp)) - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun HeroSection() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Filled.VolunteerActivism, - contentDescription = null, - modifier = - Modifier - .size(64.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.tertiary, - ), - ), - ).padding(14.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - - Spacer(Modifier.height(16.dp)) - - Text( - text = stringResource(Res.string.sponsor_hero_title), - style = MaterialTheme.typography.headlineSmallEmphasized, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - ) - - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(Res.string.sponsor_hero_subtitle), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 8.dp), - ) - - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(Res.string.sponsor_personal_note), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 8.dp), - ) - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun SponsorOptionCard( - icon: ImageVector, - title: String, - description: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - ElevatedCard( - modifier = modifier.fillMaxWidth(), - onClick = onClick, - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ), - shape = RoundedCornerShape(24.dp), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = - Modifier - .size(44.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.secondary, - ), - ), - ).padding(10.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun OtherWaysSection(onOpenUrl: (String) -> Unit) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(Res.string.sponsor_other_ways_title), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp), - ) - - Spacer(Modifier.height(4.dp)) - - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ), - shape = RoundedCornerShape(24.dp), - ) { - Column(modifier = Modifier.padding(4.dp)) { - OtherWayItem( - icon = Icons.Filled.Star, - title = stringResource(Res.string.sponsor_star_repo), - description = stringResource(Res.string.sponsor_star_repo_desc), - onClick = { - onOpenUrl("https://github.com/OpenHub-Store/GitHub-Store") - }, - ) - - OtherWayItem( - icon = Icons.Filled.BugReport, - title = stringResource(Res.string.sponsor_report_bugs), - description = stringResource(Res.string.sponsor_report_bugs_desc), - onClick = { - onOpenUrl("https://github.com/OpenHub-Store/GitHub-Store/issues") - }, - ) - - OtherWayItem( - icon = Icons.Filled.IosShare, - title = stringResource(Res.string.sponsor_share), - description = stringResource(Res.string.sponsor_share_desc), - onClick = { - onOpenUrl("https://github.com/OpenHub-Store/GitHub-Store") - }, - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun OtherWayItem( - icon: ImageVector, - title: String, - description: String, - onClick: () -> Unit, -) { - FilledTonalButton( - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = - ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt index 715d30b7d..4f05d31bc 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt @@ -21,7 +21,6 @@ import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.VolunteerActivism import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -34,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource @@ -108,14 +106,6 @@ fun LazyListScope.options( }, hasBadge = hasUnreadAnnouncements, ) - - Spacer(Modifier.height(4.dp)) - - SponsorCard( - onClick = { - onAction(ProfileAction.OnSponsorClick) - }, - ) } } @@ -245,76 +235,3 @@ private fun OptionCardContent( } } } - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun SponsorCard( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Card( - modifier = modifier, - onClick = onClick, - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - shape = RoundedCornerShape(32.dp), - border = - BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), - ), - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - imageVector = Icons.Default.VolunteerActivism, - contentDescription = null, - modifier = - Modifier - .size(36.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.tertiary, - ), - ), - ).padding(6.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - - Column( - modifier = - Modifier - .weight(1f) - .padding(12.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start, - ) { - Text( - text = stringResource(Res.string.sponsor_button), - maxLines = 1, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - - Text( - text = stringResource(Res.string.sponsor_hero_subtitle), - maxLines = 2, - style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f), - ) - } - } - } -} From 8fd083473f81eb235b1611de74925eb76ed8fc74 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:00:11 +0500 Subject: [PATCH 003/172] feat(theme): add design tokens for 4 palettes --- .../core/presentation/theme/tokens/Radii.kt | 33 ++ .../core/presentation/theme/tokens/Tokens.kt | 319 ++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt new file mode 100644 index 000000000..7476a2199 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt @@ -0,0 +1,33 @@ +package zed.rainxch.core.presentation.theme.tokens + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.dp + +/** + * Asymmetric corner-radius scale from tokens.json. Pairs are (primary, secondary) + * applied diagonally — Compose uses `RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)`. + * The handoff's `radD(primary, secondary)` translates to topStart=primary, topEnd=secondary, + * bottomEnd=primary, bottomStart=secondary. + * + * For "wonky" CTAs / lead cards / search input / sheets / dialogs / toasts, use + * [zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape] in core/components. + * Compose's `RoundedCornerShape` cannot express the elliptical (x/y differ) corners + * required for wonkiness — that lands in P5. + */ +object Radii { + val chip = shape(11, 8) + val row = shape(13, 10) + val cardSm = shape(15, 11) + val card = shape(18, 14) + val cardLg = shape(20, 15) + val hero = shape(24, 18) + val heroLg = shape(28, 22) + + /** Build an asymmetric squircle with diagonally-paired (primary, secondary) radii. */ + fun shape(primary: Int, secondary: Int) = RoundedCornerShape( + topStart = primary.dp, + topEnd = secondary.dp, + bottomEnd = primary.dp, + bottomStart = secondary.dp, + ) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt new file mode 100644 index 000000000..ca5d25d3f --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt @@ -0,0 +1,319 @@ +package zed.rainxch.core.presentation.theme.tokens + +import androidx.compose.ui.graphics.Color + +/** + * Hand-translated from `~/Downloads/handoff 4/tokens.json` (locked at DESIGN.md v1.0). + * Source of truth for the design system; do not introduce ad-hoc hex values elsewhere. + * + * Layout: palette tokens (Nord / Cream / Forest / Plum) × mode (Light / Dark / Amoled), + * status colors (palette-independent), thresholds (freshness / maintenance / stars), + * motion durations, spacing scale. + */ +object Tokens { + enum class Palette { NORD, CREAM, FOREST, PLUM } + + enum class Mode { LIGHT, DARK, AMOLED } + + data class PaletteColors( + val bg: Color, + val surface: Color, + val surface2: Color, + val ink: Color, + val ink2: Color, + val outline: Color, + val primary: Color, + val tintP: Color, + val success: Color, + val successT: Color, + val danger: Color, + val dangerT: Color, + ) + + object Nord { + val light = PaletteColors( + bg = Color(0xFFECEFF4), + surface = Color(0xFFF8FAFC), + surface2 = Color(0xFFE5E9F0), + ink = Color(0xFF2E3440), + ink2 = Color(0xFF4C566A), + outline = Color(0xFFD8DEE9), + primary = Color(0xFF5E81AC), + tintP = Color(0xFFD8E1EC), + success = Color(0xFFA3BE8C), + successT = Color(0xFFE1ECCF), + danger = Color(0xFFBF616A), + dangerT = Color(0xFFF2D6D7), + ) + val dark = PaletteColors( + bg = Color(0xFF242933), + surface = Color(0xFF2E3440), + surface2 = Color(0xFF3B4252), + ink = Color(0xFFECEFF4), + ink2 = Color(0xFFB8C0CC), + outline = Color(0xFF3B4252), + primary = Color(0xFF88C0D0), + tintP = Color(0xFF3B4252), + success = Color(0xFFA3BE8C), + successT = Color(0xFF3F4D3A), + danger = Color(0xFFBF616A), + dangerT = Color(0xFF4D2F32), + ) + val amoled = dark.copy( + bg = Color(0xFF000000), + surface = Color(0xFF0B0F14), + surface2 = Color(0xFF161B22), + ) + } + + object Cream { + val light = PaletteColors( + bg = Color(0xFFFBEFD8), + surface = Color(0xFFFFF9EC), + surface2 = Color(0xFFF3E4C6), + ink = Color(0xFF2B1F14), + ink2 = Color(0xFF7A6549), + outline = Color(0xFFE6D5B8), + primary = Color(0xFFB8542C), + tintP = Color(0xFFFFE7CB), + success = Color(0xFF7B8E4A), + successT = Color(0xFFE0E5CB), + danger = Color(0xFFB83A2C), + dangerT = Color(0xFFF3D7CF), + ) + val dark = PaletteColors( + bg = Color(0xFF241910), + surface = Color(0xFF332419), + surface2 = Color(0xFF473324), + ink = Color(0xFFFBEFD8), + ink2 = Color(0xFFC7B196), + outline = Color(0xFF473324), + primary = Color(0xFFE89968), + tintP = Color(0xFF473324), + success = Color(0xFFA3BE8C), + successT = Color(0xFF3F4D3A), + danger = Color(0xFFD26B5A), + dangerT = Color(0xFF4D2922), + ) + val amoled = dark.copy( + bg = Color(0xFF000000), + surface = Color(0xFF120A05), + surface2 = Color(0xFF1F140C), + ) + } + + object Forest { + val light = PaletteColors( + bg = Color(0xFFEDF1E8), + surface = Color(0xFFF7FAF3), + surface2 = Color(0xFFDDE5D2), + ink = Color(0xFF2D3A2C), + ink2 = Color(0xFF5A6A57), + outline = Color(0xFFCFD9C2), + primary = Color(0xFF6B8E5A), + tintP = Color(0xFFDCE7CE), + success = Color(0xFF6B8E5A), + successT = Color(0xFFDCE7CE), + danger = Color(0xFFB83A2C), + dangerT = Color(0xFFF3D6D2), + ) + val dark = PaletteColors( + bg = Color(0xFF1D241D), + surface = Color(0xFF272F26), + surface2 = Color(0xFF363F35), + ink = Color(0xFFEDF1E8), + ink2 = Color(0xFFB5C2AE), + outline = Color(0xFF363F35), + primary = Color(0xFFA3BE8C), + tintP = Color(0xFF363F35), + success = Color(0xFFA3BE8C), + successT = Color(0xFF3D4D3A), + danger = Color(0xFFD26B5A), + dangerT = Color(0xFF4D2922), + ) + val amoled = dark.copy( + bg = Color(0xFF000000), + surface = Color(0xFF0A0F0A), + surface2 = Color(0xFF131A12), + ) + } + + object Plum { + val light = PaletteColors( + bg = Color(0xFFEDE8EF), + surface = Color(0xFFF7F2F9), + surface2 = Color(0xFFDCD5E0), + ink = Color(0xFF322A36), + ink2 = Color(0xFF5C546A), + outline = Color(0xFFCFC8D6), + primary = Color(0xFF7E6BA8), + tintP = Color(0xFFDCD7E7), + success = Color(0xFF7B8E4A), + successT = Color(0xFFDBE5C7), + danger = Color(0xFFB83A2C), + dangerT = Color(0xFFF3D6D2), + ) + val dark = PaletteColors( + bg = Color(0xFF1D1A22), + surface = Color(0xFF26222D), + surface2 = Color(0xFF34303D), + ink = Color(0xFFEDE8EF), + ink2 = Color(0xFFB5ACBC), + outline = Color(0xFF34303D), + primary = Color(0xFFB39CD4), + tintP = Color(0xFF34303D), + success = Color(0xFFA3BE8C), + successT = Color(0xFF3F4D3A), + danger = Color(0xFFD26B5A), + dangerT = Color(0xFF4D2922), + ) + val amoled = dark.copy( + bg = Color(0xFF000000), + surface = Color(0xFF0C0A10), + surface2 = Color(0xFF161320), + ) + } + + fun palette(p: Palette, m: Mode): PaletteColors = when (p) { + Palette.NORD -> when (m) { + Mode.LIGHT -> Nord.light + Mode.DARK -> Nord.dark + Mode.AMOLED -> Nord.amoled + } + Palette.CREAM -> when (m) { + Mode.LIGHT -> Cream.light + Mode.DARK -> Cream.dark + Mode.AMOLED -> Cream.amoled + } + Palette.FOREST -> when (m) { + Mode.LIGHT -> Forest.light + Mode.DARK -> Forest.dark + Mode.AMOLED -> Forest.amoled + } + Palette.PLUM -> when (m) { + Mode.LIGHT -> Plum.light + Mode.DARK -> Plum.dark + Mode.AMOLED -> Plum.amoled + } + } + + /** Palette-independent status colors. Same hex regardless of theme. */ + object Status { + object Freshness { + val hot = Color(0xFFE07856) + val fresh = Color(0xFF6BA068) + val warm = Color(0xFFC49652) + val cool = Color(0xFF8E8E8E) + val dormant = Color(0xFF9E9E9E) + } + object Wax { + val intact = Color(0xFF8B4A2B) + val cracked = Color(0xFFB83A2C) + val open = Color(0xFF8E8E8E) + } + object Perm { + val low = Color(0xFF6BA068) + val moderate = Color(0xFFC49652) + val high = Color(0xFFB83A2C) + } + object Trend { + val rising = Color(0xFF6BA068) + val flat = Color(0xFF8E8E8E) + val falling = Color(0xFFB83A2C) + } + } + + /** Bucket thresholds and ring-fraction targets from tokens.json. */ + object Thresholds { + data class FreshnessBucket( + val maxDaysInclusive: Int?, + val state: String, + val ringFraction: Float, + val color: Color, + ) + val freshness = listOf( + FreshnessBucket(3, "hot", 1.00f, Status.Freshness.hot), + FreshnessBucket(30, "fresh", 0.78f, Status.Freshness.fresh), + FreshnessBucket(90, "warm", 0.55f, Status.Freshness.warm), + FreshnessBucket(365, "cool", 0.30f, Status.Freshness.cool), + FreshnessBucket(null, "dormant", 0.12f, Status.Freshness.dormant), + ) + + data class StarTier(val minStars: Int, val tier: Int) + val stars = listOf( + StarTier(0, 1), + StarTier(1000, 2), + StarTier(10000, 3), + StarTier(50000, 4), + StarTier(100000, 5), + ) + + data class MaintenanceBucket( + val maxDaysInclusive: Int?, + val state: String, + val heartbeatPeriodMs: Int?, + val color: Color, + ) + val maintenance = listOf( + MaintenanceBucket(1, "active", 1400, Status.Freshness.fresh), + MaintenanceBucket(7, "recent", 2400, Status.Freshness.fresh), + MaintenanceBucket(30, "quiet", 4200, Status.Freshness.warm), + MaintenanceBucket(null, "dormant", null, Status.Freshness.dormant), + ) + } + + /** Motion (durations in ms; pair with [Motion.heartbeatScaleFrom/To] for the breathing dot). */ + object Motion { + const val tapHighlightMs = 120 + const val paletteCrossfadeMs = 250 + const val sheetSlideMs = 240 + const val scrimFadeMs = 180 + const val toastSlideMs = 200 + const val toastFadeMs = 240 + const val heartbeatScaleFrom = 1.0f + const val heartbeatScaleTo = 1.25f + const val heartbeatHaloFromScale = 1.0f + const val heartbeatHaloToScale = 2.4f + const val heartbeatHaloFromAlpha = 0.45f + const val heartbeatHaloToAlpha = 0.0f + } + + /** Spacing scale (Material 3 aligned defaults; expand only if a screen needs it). */ + object Spacing { + val xs = 4 + val sm = 8 + val md = 12 + val lg = 16 + val xl = 24 + val xxl = 32 + } + + /** + * Topic glyphs supported by [TopicGlyph]. Aliases map non-canonical topic strings + * to a supported glyph (e.g. "password-manager" → "key"). + */ + object Topics { + val supported = setOf( + "self-hosted", "mobile", "photo", "video", "book", "manga", + "key", "audio", "backup", "reader", "cross-platform", "cloud", + ) + val aliases = mapOf( + "password-manager" to "key", + "podcast" to "audio", + "ebook" to "book", + "messaging" to "key", + "vpn" to "cloud", + "note" to "book", + ) + } + + /** SPDX → posture map for [LicensePosture]. */ + object Licenses { + val copyleft = setOf( + "AGPL-3.0", "GPL-3.0", "GPL-2.0", "LGPL-3.0", "LGPL-2.1", "MPL-2.0", + ) + val permissive = setOf( + "MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unlicense", + ) + } +} From 167a85d5de94dbfea343b1ab761d18540aea36bb Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:10:48 +0500 Subject: [PATCH 004/172] feat(theme): swap to Nord/Cream/Forest/Plum palettes --- .../zed/rainxch/githubstore/MainState.kt | 2 +- .../zed/rainxch/core/domain/model/AppTheme.kt | 32 +- .../rainxch/core/domain/model/ThemeMode.kt | 21 + .../core/presentation/theme/Theme.android.kt | 24 - .../composeResources/values-ar/strings-ar.xml | 8 +- .../composeResources/values-bn/strings-bn.xml | 8 +- .../composeResources/values-es/strings-es.xml | 8 +- .../composeResources/values-fr/strings-fr.xml | 8 +- .../composeResources/values-hi/strings-hi.xml | 8 +- .../composeResources/values-it/strings-it.xml | 8 +- .../composeResources/values-ja/strings-ja.xml | 8 +- .../composeResources/values-ko/strings-ko.xml | 8 +- .../composeResources/values-pl/strings-pl.xml | 8 +- .../composeResources/values-ru/strings-ru.xml | 8 +- .../composeResources/values-tr/strings-tr.xml | 8 +- .../values-zh-rCN/strings-zh-rCN.xml | 8 +- .../composeResources/values/strings.xml | 8 +- .../rainxch/core/presentation/theme/Color.kt | 90 ---- .../rainxch/core/presentation/theme/Theme.kt | 458 +----------------- .../core/presentation/theme/tokens/Schemes.kt | 109 +++++ .../core/presentation/utils/AppThemeUtil.kt | 89 ++-- .../core/presentation/theme/Theme.jvm.kt | 9 - .../tweaks/presentation/TweaksState.kt | 2 +- .../components/sections/Appearance.kt | 27 +- 24 files changed, 251 insertions(+), 716 deletions(-) create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt delete mode 100644 core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/Theme.android.kt delete mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Color.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt delete mode 100644 core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/Theme.jvm.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt index 8976ec784..06ea4cd4f 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt @@ -10,7 +10,7 @@ data class MainState( val rateLimitInfo: RateLimitInfo? = null, val showRateLimitDialog: Boolean = false, val showSessionExpiredDialog: Boolean = false, - val currentColorTheme: AppTheme = AppTheme.OCEAN, + val currentColorTheme: AppTheme = AppTheme.NORD, val isAmoledTheme: Boolean = false, val isDarkTheme: Boolean? = null, val currentFontTheme: FontTheme = FontTheme.CUSTOM, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt index 0024a084c..ccd3a9ad2 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt @@ -1,15 +1,35 @@ package zed.rainxch.core.domain.model +/** + * Palette identity. Each entry maps 1:1 to a [zed.rainxch.core.presentation.theme.tokens.Tokens.Palette] + * at the presentation layer. Light / Dark / Amoled are orthogonal — see [ThemeMode]. + * + * Legacy values from the pre-overhaul enum (DYNAMIC / OCEAN / PURPLE / SLATE / AMBER) are + * migrated on first read by [fromName] — see [LEGACY_MIGRATION] for the explicit map. + * Material You dynamic color is intentionally dropped (themes.md "Disallowed combinations"). + */ enum class AppTheme { - DYNAMIC, - OCEAN, - PURPLE, + NORD, + CREAM, FOREST, - SLATE, - AMBER, + PLUM, ; companion object { - fun fromName(name: String?): AppTheme = entries.find { it.name == name } ?: OCEAN + /** Legacy → new palette mapping. Locked in `.design/DECISIONS.md` D3. */ + private val LEGACY_MIGRATION = mapOf( + "DYNAMIC" to NORD, + "OCEAN" to NORD, + "SLATE" to NORD, + "PURPLE" to PLUM, + "AMBER" to CREAM, + ) + + fun fromName(name: String?): AppTheme { + if (name.isNullOrEmpty()) return NORD + entries.firstOrNull { it.name == name }?.let { return it } + LEGACY_MIGRATION[name]?.let { return it } + return NORD + } } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt new file mode 100644 index 000000000..9380460a9 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt @@ -0,0 +1,21 @@ +package zed.rainxch.core.domain.model + +/** + * Light / Dark / Amoled / System — orthogonal to [AppTheme] palette. AMOLED is a Dark + * sub-mode (pure-black background); falls back to DARK rendering when SYSTEM resolves + * to dark and AMOLED preference is enabled separately. + */ +enum class ThemeMode { + LIGHT, + DARK, + AMOLED, + SYSTEM, + ; + + companion object { + fun fromName(name: String?): ThemeMode { + if (name.isNullOrEmpty()) return SYSTEM + return entries.firstOrNull { it.name == name } ?: SYSTEM + } + } +} diff --git a/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/Theme.android.kt b/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/Theme.android.kt deleted file mode 100644 index f3068611d..000000000 --- a/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/Theme.android.kt +++ /dev/null @@ -1,24 +0,0 @@ -package zed.rainxch.core.presentation.theme - -import android.os.Build -import androidx.annotation.ChecksSdkIntAtLeast -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) -actual fun isDynamicColorAvailable(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - -@Composable -actual fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme? { - if (!isDynamicColorAvailable()) return null - - val context = LocalContext.current - return if (darkTheme) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - } -} diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 1c029620d..96ff5dbed 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -187,12 +187,10 @@ هل أنت متأكد أنك تريد تسجيل الخروج؟ - ديناميكي - محيط - بنفسجي غابة - رمادي - كهرماني + Nord + Cream + Plum فتح المستودع diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 3c1a044ce..fc4e54cf5 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -149,12 +149,10 @@ আপনি কি নিশ্চিতভাবে লগআউট করতে চান? - ডাইনামিক - সমুদ্র - বেগুনি বন - স্লেট - অ্যাম্বার + Nord + Cream + Plum রিপোজিটরি খুলুন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 384a0832e..5a43d6396 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -125,12 +125,10 @@ ¡Advertencia! ¿Estás seguro de que deseas cerrar sesión? - Dinámico - Océano - Púrpura Bosque - Pizarra - Ámbar + Nord + Cream + Plum Error al cargar detalles Acerca de esta app diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 058d77e26..7628d8d57 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -125,12 +125,10 @@ Attention ! Voulez-vous vraiment vous déconnecter ? - Dynamique - Océan - Violet Forêt - Ardoise - Ambre + Nord + Cream + Plum Erreur de chargement À propos de cette application diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index f1f043297..e1f15d174 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -149,12 +149,10 @@ क्या आप लॉग आउट करना चाहते हैं? - डायनामिक - ओशन - पर्पल फॉरेस्ट - स्लेट - एम्बर + Nord + Cream + Plum रिपॉजिटरी खोलें diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 949679c5d..26b5aadb7 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -149,12 +149,10 @@ Sei sicuro di voler uscire? - Dinamico - Oceano - Viola Foresta - Ardesia - Ambra + Nord + Cream + Plum Apri repository diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index c3238263f..ef0f1f8e1 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -125,12 +125,10 @@ 警告! ログアウトしてもよろしいですか? - ダイナミック - オーシャン - パープル フォレスト - スレート - アンバー + Nord + Cream + Plum 詳細の読み込みに失敗しました このアプリについて diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 535e0981b..579407cfe 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -147,12 +147,10 @@ 정말 로그아웃하시겠습니까? - 동적 - 오션 - 퍼플 포레스트 - 슬레이트 - 앰버 + Nord + Cream + Plum 저장소 열기 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index be76131ef..953ac5b99 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -126,12 +126,10 @@ Ostrzeżenie! Czy na pewno chcesz się wylogować? - Dynamiczny - Ocean - Fioletowy Las - Łupek - Bursztyn + Nord + Cream + Plum Otwórz repozytorium Otwórz w przeglądarce diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 40b594071..3969fbfd9 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -125,12 +125,10 @@ Внимание! Вы уверены, что хотите выйти? - Динамическая - Океан - Фиолетовая Лесная - Сланцевая - Янтарная + Nord + Cream + Plum Открыть репозиторий Отменить загрузку diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index b6d555731..1426e5b7b 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -148,12 +148,10 @@ Çıkmak istediğinden emin misin? - Dinamik - Deniz - Mor Orman - Arduvaz - Amber + Nord + Cream + Plum Repoyu Aç diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 97e3872e1..cd8fb2263 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -127,12 +127,10 @@ 警告! 确定要退出登录吗? - 动态 - 海洋 - 紫色 森林 - 石板 - 琥珀 + Nord + Cream + Plum 加载详情失败 关于此应用 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index b68abea24..def041cf5 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -189,12 +189,10 @@ Are you sure you want to log out? - Dynamic - Ocean - Purple Forest - Slate - Amber + Nord + Cream + Plum Open repository diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Color.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Color.kt deleted file mode 100644 index f76a68801..000000000 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Color.kt +++ /dev/null @@ -1,90 +0,0 @@ -package zed.rainxch.core.presentation.theme - -import androidx.compose.material3.ColorScheme -import androidx.compose.ui.graphics.Color - -val primaryLight = Color(0xFF2A638A) -val onPrimaryLight = Color(0xFFFFFFFF) -val primaryContainerLight = Color(0xFFCBE6FF) -val onPrimaryContainerLight = Color(0xFF034B71) -val secondaryLight = Color(0xFF50606F) -val onSecondaryLight = Color(0xFFFFFFFF) -val secondaryContainerLight = Color(0xFFD4E4F6) -val onSecondaryContainerLight = Color(0xFF394856) -val tertiaryLight = Color(0xFF66587B) -val onTertiaryLight = Color(0xFFFFFFFF) -val tertiaryContainerLight = Color(0xFFECDCFF) -val onTertiaryContainerLight = Color(0xFF4E4162) -val errorLight = Color(0xFFBA1A1A) -val onErrorLight = Color(0xFFFFFFFF) -val errorContainerLight = Color(0xFFFFDAD6) -val onErrorContainerLight = Color(0xFF93000A) -val backgroundLight = Color(0xFFF7F9FF) -val onBackgroundLight = Color(0xFF181C20) -val surfaceLight = Color(0xFFF7F9FF) -val onSurfaceLight = Color(0xFF181C20) -val surfaceVariantLight = Color(0xFFDEE3EA) -val onSurfaceVariantLight = Color(0xFF42474D) -val outlineLight = Color(0xFF72787E) -val outlineVariantLight = Color(0xFFC1C7CE) -val scrimLight = Color(0xFF000000) -val inverseSurfaceLight = Color(0xFF2D3135) -val inverseOnSurfaceLight = Color(0xFFEEF1F6) -val inversePrimaryLight = Color(0xFF98CCF9) -val surfaceDimLight = Color(0xFFD7DADF) -val surfaceBrightLight = Color(0xFFF7F9FF) -val surfaceContainerLowestLight = Color(0xFFFFFFFF) -val surfaceContainerLowLight = Color(0xFFF1F4F9) -val surfaceContainerLight = Color(0xFFEBEEF3) -val surfaceContainerHighLight = Color(0xFFE6E8EE) -val surfaceContainerHighestLight = Color(0xFFE0E3E8) - -val primaryDark = Color(0xFF98CCF9) -val onPrimaryDark = Color(0xFF003350) -val primaryContainerDark = Color(0xFF034B71) -val onPrimaryContainerDark = Color(0xFFCBE6FF) -val secondaryDark = Color(0xFFB8C8D9) -val onSecondaryDark = Color(0xFF22323F) -val secondaryContainerDark = Color(0xFF394856) -val onSecondaryContainerDark = Color(0xFFD4E4F6) -val tertiaryDark = Color(0xFFD1BFE7) -val onTertiaryDark = Color(0xFF372B4A) -val tertiaryContainerDark = Color(0xFF4E4162) -val onTertiaryContainerDark = Color(0xFFECDCFF) -val errorDark = Color(0xFFFFB4AB) -val onErrorDark = Color(0xFF690005) -val errorContainerDark = Color(0xFF93000A) -val onErrorContainerDark = Color(0xFFFFDAD6) -val backgroundDark = Color(0xFF101417) -val onBackgroundDark = Color(0xFFE0E3E8) -val surfaceDark = Color(0xFF101417) -val onSurfaceDark = Color(0xFFE0E3E8) -val surfaceVariantDark = Color(0xFF42474D) -val onSurfaceVariantDark = Color(0xFFC1C7CE) -val outlineDark = Color(0xFF8C9198) -val outlineVariantDark = Color(0xFF42474D) -val scrimDark = Color(0xFF000000) -val inverseSurfaceDark = Color(0xFFE0E3E8) -val inverseOnSurfaceDark = Color(0xFF2D3135) -val inversePrimaryDark = Color(0xFF2A638A) -val surfaceDimDark = Color(0xFF101417) -val surfaceBrightDark = Color(0xFF363A3E) -val surfaceContainerLowestDark = Color(0xFF0B0F12) -val surfaceContainerLowDark = Color(0xFF181C20) -val surfaceContainerDark = Color(0xFF1C2024) -val surfaceContainerHighDark = Color(0xFF272A2E) -val surfaceContainerHighestDark = Color(0xFF313539) - -fun ColorScheme.toAmoled(): ColorScheme = - this.copy( - background = Color.Black, - surface = Color.Black, - surfaceContainer = Color(0xFF0A0A0A), - surfaceContainerLow = Color(0xFF050505), - surfaceContainerLowest = Color.Black, - surfaceContainerHigh = Color(0xFF121212), - surfaceContainerHighest = Color(0xFF1A1A1A), - surfaceDim = Color(0xFF0D0D0D), - surfaceBright = Color(0xFF1F1F1F), - surfaceVariant = Color(0xFF121212), - ) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index 2687af250..ccbbeb8f9 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -1,461 +1,45 @@ package zed.rainxch.core.presentation.theme -import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.FontTheme -import zed.rainxch.core.presentation.utils.darkScheme -import zed.rainxch.core.presentation.utils.lightScheme - -val oceanBlueLight = - lightColorScheme( - primary = primaryLight, - onPrimary = onPrimaryLight, - primaryContainer = primaryContainerLight, - onPrimaryContainer = onPrimaryContainerLight, - secondary = secondaryLight, - onSecondary = onSecondaryLight, - secondaryContainer = secondaryContainerLight, - onSecondaryContainer = onSecondaryContainerLight, - tertiary = tertiaryLight, - onTertiary = onTertiaryLight, - tertiaryContainer = tertiaryContainerLight, - onTertiaryContainer = onTertiaryContainerLight, - error = errorLight, - onError = onErrorLight, - errorContainer = errorContainerLight, - onErrorContainer = onErrorContainerLight, - background = backgroundLight, - onBackground = onBackgroundLight, - surface = surfaceLight, - onSurface = onSurfaceLight, - surfaceVariant = surfaceVariantLight, - onSurfaceVariant = onSurfaceVariantLight, - outline = outlineLight, - outlineVariant = outlineVariantLight, - scrim = scrimLight, - inverseSurface = inverseSurfaceLight, - inverseOnSurface = inverseOnSurfaceLight, - inversePrimary = inversePrimaryLight, - surfaceDim = surfaceDimLight, - surfaceBright = surfaceBrightLight, - surfaceContainerLowest = surfaceContainerLowestLight, - surfaceContainerLow = surfaceContainerLowLight, - surfaceContainer = surfaceContainerLight, - surfaceContainerHigh = surfaceContainerHighLight, - surfaceContainerHighest = surfaceContainerHighestLight, - ) - -val oceanBlueDark = - darkColorScheme( - primary = primaryDark, - onPrimary = onPrimaryDark, - primaryContainer = primaryContainerDark, - onPrimaryContainer = onPrimaryContainerDark, - secondary = secondaryDark, - onSecondary = onSecondaryDark, - secondaryContainer = secondaryContainerDark, - onSecondaryContainer = onSecondaryContainerDark, - tertiary = tertiaryDark, - onTertiary = onTertiaryDark, - tertiaryContainer = tertiaryContainerDark, - onTertiaryContainer = onTertiaryContainerDark, - error = errorDark, - onError = onErrorDark, - errorContainer = errorContainerDark, - onErrorContainer = onErrorContainerDark, - background = backgroundDark, - onBackground = onBackgroundDark, - surface = surfaceDark, - onSurface = onSurfaceDark, - surfaceVariant = surfaceVariantDark, - onSurfaceVariant = onSurfaceVariantDark, - outline = outlineDark, - outlineVariant = outlineVariantDark, - scrim = scrimDark, - inverseSurface = inverseSurfaceDark, - inverseOnSurface = inverseOnSurfaceDark, - inversePrimary = inversePrimaryDark, - surfaceDim = surfaceDimDark, - surfaceBright = surfaceBrightDark, - surfaceContainerLowest = surfaceContainerLowestDark, - surfaceContainerLow = surfaceContainerLowDark, - surfaceContainer = surfaceContainerDark, - surfaceContainerHigh = surfaceContainerHighDark, - surfaceContainerHighest = surfaceContainerHighestDark, - ) - -val deepPurpleLight = - lightColorScheme( - primary = Color(0xFF6750A4), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFE9DDFF), - onPrimaryContainer = Color(0xFF22005D), - secondary = Color(0xFF625B71), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFE8DEF8), - onSecondaryContainer = Color(0xFF1E192B), - tertiary = Color(0xFF7E5260), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFFFD9E3), - onTertiaryContainer = Color(0xFF31101D), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFFFFBFF), - onBackground = Color(0xFF1C1B1E), - surface = Color(0xFFFFFBFF), - onSurface = Color(0xFF1C1B1E), - surfaceVariant = Color(0xFFE7E0EB), - onSurfaceVariant = Color(0xFF49454E), - outline = Color(0xFF7A757F), - outlineVariant = Color(0xFFCAC4CF), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF313033), - inverseOnSurface = Color(0xFFF4EFF4), - inversePrimary = Color(0xFFCFBCFF), - surfaceDim = Color(0xFFDED8DD), - surfaceBright = Color(0xFFFFFBFF), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF8F2F7), - surfaceContainer = Color(0xFFF2ECF1), - surfaceContainerHigh = Color(0xFFECE6EB), - surfaceContainerHighest = Color(0xFFE6E1E6), - ) - -val deepPurpleDark = - darkColorScheme( - primary = Color(0xFFCFBCFF), - onPrimary = Color(0xFF381E72), - primaryContainer = Color(0xFF4F378A), - onPrimaryContainer = Color(0xFFE9DDFF), - secondary = Color(0xFFCBC2DB), - onSecondary = Color(0xFF332D41), - secondaryContainer = Color(0xFF4A4458), - onSecondaryContainer = Color(0xFFE8DEF8), - tertiary = Color(0xFFEFB8C8), - onTertiary = Color(0xFF4A2532), - tertiaryContainer = Color(0xFF633B48), - onTertiaryContainer = Color(0xFFFFD9E3), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF141316), - onBackground = Color(0xFFE6E1E6), - surface = Color(0xFF141316), - onSurface = Color(0xFFE6E1E6), - surfaceVariant = Color(0xFF49454E), - onSurfaceVariant = Color(0xFFCAC4CF), - outline = Color(0xFF948F99), - outlineVariant = Color(0xFF49454E), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFE6E1E6), - inverseOnSurface = Color(0xFF313033), - inversePrimary = Color(0xFF6750A4), - surfaceDim = Color(0xFF141316), - surfaceBright = Color(0xFF3A383C), - surfaceContainerLowest = Color(0xFF0F0E11), - surfaceContainerLow = Color(0xFF1C1B1E), - surfaceContainer = Color(0xFF201F22), - surfaceContainerHigh = Color(0xFF2B292D), - surfaceContainerHighest = Color(0xFF363438), - ) - -// ============================================================================ -// FOREST GREEN THEME (Trusted & Verified) -// ============================================================================ -val forestGreenLight = - lightColorScheme( - primary = Color(0xFF356859), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFB8EED9), - onPrimaryContainer = Color(0xFF002019), - secondary = Color(0xFF4C6359), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFCEE9DB), - onSecondaryContainer = Color(0xFF092018), - tertiary = Color(0xFF3F6373), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFC3E8FB), - onTertiaryContainer = Color(0xFF001F29), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFF5FBF7), - onBackground = Color(0xFF171D1A), - surface = Color(0xFFF5FBF7), - onSurface = Color(0xFF171D1A), - surfaceVariant = Color(0xFFDBE5DD), - onSurfaceVariant = Color(0xFF404943), - outline = Color(0xFF707973), - outlineVariant = Color(0xFFBFC9C1), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF2C322F), - inverseOnSurface = Color(0xFFEDF2ED), - inversePrimary = Color(0xFF9CD1BD), - surfaceDim = Color(0xFFD6DBD8), - surfaceBright = Color(0xFFF5FBF7), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF0F5F1), - surfaceContainer = Color(0xFFEAEFEB), - surfaceContainerHigh = Color(0xFFE4E9E6), - surfaceContainerHighest = Color(0xFFDFE4E0), - ) - -val forestGreenDark = - darkColorScheme( - primary = Color(0xFF9CD1BD), - onPrimary = Color(0xFF00382B), - primaryContainer = Color(0xFF1C4F41), - onPrimaryContainer = Color(0xFFB8EED9), - secondary = Color(0xFFB2CDBF), - onSecondary = Color(0xFF1D352C), - secondaryContainer = Color(0xFF344C42), - onSecondaryContainer = Color(0xFFCEE9DB), - tertiary = Color(0xFFA7CCDE), - onTertiary = Color(0xFF0C3443), - tertiaryContainer = Color(0xFF264B5B), - onTertiaryContainer = Color(0xFFC3E8FB), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF0F1512), - onBackground = Color(0xFFDFE4E0), - surface = Color(0xFF0F1512), - onSurface = Color(0xFFDFE4E0), - surfaceVariant = Color(0xFF404943), - onSurfaceVariant = Color(0xFFBFC9C1), - outline = Color(0xFF89938C), - outlineVariant = Color(0xFF404943), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFDFE4E0), - inverseOnSurface = Color(0xFF2C322F), - inversePrimary = Color(0xFF356859), - surfaceDim = Color(0xFF0F1512), - surfaceBright = Color(0xFF353B37), - surfaceContainerLowest = Color(0xFF0A100D), - surfaceContainerLow = Color(0xFF171D1A), - surfaceContainer = Color(0xFF1B211E), - surfaceContainerHigh = Color(0xFF262C28), - surfaceContainerHighest = Color(0xFF313733), - ) - -// ============================================================================ -// SLATE GRAY THEME (Minimal Developer) -// ============================================================================ -val slateGrayLight = - lightColorScheme( - primary = Color(0xFF535E6C), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFD7E3F3), - onPrimaryContainer = Color(0xFF101C27), - secondary = Color(0xFF565E6B), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFDAE2F1), - onSecondaryContainer = Color(0xFF131C26), - tertiary = Color(0xFF6E5676), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFF7D9FF), - onTertiaryContainer = Color(0xFF281430), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFF8F9FB), - onBackground = Color(0xFF191C1E), - surface = Color(0xFFF8F9FB), - onSurface = Color(0xFF191C1E), - surfaceVariant = Color(0xFFDFE2E9), - onSurfaceVariant = Color(0xFF43474E), - outline = Color(0xFF73777F), - outlineVariant = Color(0xFFC3C6CD), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF2E3133), - inverseOnSurface = Color(0xFFF0F0F3), - inversePrimary = Color(0xFFB4C7D9), - surfaceDim = Color(0xFFD9D9DC), - surfaceBright = Color(0xFFF8F9FB), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF3F3F6), - surfaceContainer = Color(0xFFEDEDF0), - surfaceContainerHigh = Color(0xFFE7E8EA), - surfaceContainerHighest = Color(0xFFE2E2E5), - ) - -val slateGrayDark = - darkColorScheme( - primary = Color(0xFFB4C7D9), - onPrimary = Color(0xFF1F2F3D), - primaryContainer = Color(0xFF394654), - onPrimaryContainer = Color(0xFFD7E3F3), - secondary = Color(0xFFBEC6D5), - onSecondary = Color(0xFF28323B), - secondaryContainer = Color(0xFF3E4753), - onSecondaryContainer = Color(0xFFDAE2F1), - tertiary = Color(0xFFDABDE2), - onTertiary = Color(0xFF3E2946), - tertiaryContainer = Color(0xFF553F5D), - onTertiaryContainer = Color(0xFFF7D9FF), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF111416), - onBackground = Color(0xFFE2E2E5), - surface = Color(0xFF111416), - onSurface = Color(0xFFE2E2E5), - surfaceVariant = Color(0xFF43474E), - onSurfaceVariant = Color(0xFFC3C6CD), - outline = Color(0xFF8D9199), - outlineVariant = Color(0xFF43474E), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFE2E2E5), - inverseOnSurface = Color(0xFF2E3133), - inversePrimary = Color(0xFF535E6C), - surfaceDim = Color(0xFF111416), - surfaceBright = Color(0xFF37393B), - surfaceContainerLowest = Color(0xFF0C0F11), - surfaceContainerLow = Color(0xFF191C1E), - surfaceContainer = Color(0xFF1D2022), - surfaceContainerHigh = Color(0xFF282A2D), - surfaceContainerHighest = Color(0xFF333538), - ) - -// ============================================================================ -// AMBER ORANGE THEME (Energetic & Warm) -// ============================================================================ -val amberOrangeLight = - lightColorScheme( - primary = Color(0xFF8B5000), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFFFDCBE), - onPrimaryContainer = Color(0xFF2D1600), - secondary = Color(0xFF715A48), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFFFDCBE), - onSecondaryContainer = Color(0xFF28190A), - tertiary = Color(0xFF54643D), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFD7E9B8), - onTertiaryContainer = Color(0xFF131F02), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF410002), - background = Color(0xFFFFFBFF), - onBackground = Color(0xFF201B16), - surface = Color(0xFFFFFBFF), - onSurface = Color(0xFF201B16), - surfaceVariant = Color(0xFFF2DFD1), - onSurfaceVariant = Color(0xFF51443A), - outline = Color(0xFF837469), - outlineVariant = Color(0xFFD5C3B6), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF36302A), - inverseOnSurface = Color(0xFFFAEFE7), - inversePrimary = Color(0xFFFFB870), - surfaceDim = Color(0xFFE4D9D1), - surfaceBright = Color(0xFFFFFBFF), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFFEF3EB), - surfaceContainer = Color(0xFFF8EDE5), - surfaceContainerHigh = Color(0xFFF2E7DF), - surfaceContainerHighest = Color(0xFFECE1DA), - ) - -val amberOrangeDark = - darkColorScheme( - primary = Color(0xFFFFB870), - onPrimary = Color(0xFF4B2800), - primaryContainer = Color(0xFF6A3C00), - onPrimaryContainer = Color(0xFFFFDCBE), - secondary = Color(0xFFE2C1A3), - onSecondary = Color(0xFF402D1D), - secondaryContainer = Color(0xFF584332), - onSecondaryContainer = Color(0xFFFFDCBE), - tertiary = Color(0xFFBBCD9E), - onTertiary = Color(0xFF273514), - tertiaryContainer = Color(0xFF3D4C28), - onTertiaryContainer = Color(0xFFD7E9B8), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF18130E), - onBackground = Color(0xFFECE1DA), - surface = Color(0xFF18130E), - onSurface = Color(0xFFECE1DA), - surfaceVariant = Color(0xFF51443A), - onSurfaceVariant = Color(0xFFD5C3B6), - outline = Color(0xFF9D8E82), - outlineVariant = Color(0xFF51443A), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFECE1DA), - inverseOnSurface = Color(0xFF36302A), - inversePrimary = Color(0xFF8B5000), - surfaceDim = Color(0xFF18130E), - surfaceBright = Color(0xFF3F3933), - surfaceContainerLowest = Color(0xFF120E09), - surfaceContainerLow = Color(0xFF201B16), - surfaceContainer = Color(0xFF241F1A), - surfaceContainerHigh = Color(0xFF2F2A24), - surfaceContainerHighest = Color(0xFF3A342E), - ) - +import zed.rainxch.core.presentation.theme.tokens.Tokens +import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor +import zed.rainxch.core.presentation.utils.toTokenPalette + +/** + * App-wide theme entry point. Resolves the active [AppTheme] palette + light/dark/amoled + * mode to a Material 3 [ColorScheme] backed by the design tokens in + * [zed.rainxch.core.presentation.theme.tokens.Tokens]. + * + * Composition locals exposing the richer token surface (status colors, thresholds, motion, + * spacing) are added in the upcoming `GhsTheme` wrapper — kept here as a thin shim for the + * existing `Main.kt` call site until P6 chrome polish swaps in the new composable. + */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun GithubStoreTheme( isDarkTheme: Boolean = false, - appTheme: AppTheme = AppTheme.OCEAN, + appTheme: AppTheme = AppTheme.NORD, fontTheme: FontTheme = FontTheme.CUSTOM, isAmoledTheme: Boolean = false, content: @Composable () -> Unit, ) { - val baseColorScheme = - when { - appTheme == AppTheme.DYNAMIC -> { - getDynamicColorScheme(isDarkTheme) ?: run { - if (isDarkTheme) AppTheme.OCEAN.darkScheme else AppTheme.OCEAN.lightScheme - } - } - - isDarkTheme -> { - appTheme.darkScheme!! - } - - else -> { - appTheme.lightScheme!! - } - } - - val colorScheme = - if (isDarkTheme && isAmoledTheme) { - baseColorScheme?.toAmoled() - } else { - baseColorScheme - } - + val mode = when { + !isDarkTheme -> Tokens.Mode.LIGHT + isAmoledTheme -> Tokens.Mode.AMOLED + else -> Tokens.Mode.DARK + } + val scheme = colorSchemeFor(palette = appTheme.toTokenPalette(), mode = mode) MaterialExpressiveTheme( - colorScheme = colorScheme, + colorScheme = scheme, typography = getAppTypography(fontTheme), motionScheme = MotionScheme.expressive(), shapes = MaterialTheme.shapes, content = content, ) } - -expect fun isDynamicColorAvailable(): Boolean - -@Composable -expect fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme? diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt new file mode 100644 index 000000000..91426f6b2 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt @@ -0,0 +1,109 @@ +package zed.rainxch.core.presentation.theme.tokens + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +/** + * Maps a [Tokens.PaletteColors] to a Material 3 [ColorScheme] so existing M3 components + * (Button, Card, TextField, ...) keep working unchanged. Tokens not natively expressed + * in M3 (status colors, custom motion, shape radii) are exposed via the composition + * locals provided by GhsTheme — this mapper only covers M3 slots. + * + * Mapping rules: + * bg → background + * surface → surface / surfaceContainerLow / surfaceBright + * surface2 → surfaceVariant / surfaceContainer / surfaceContainerHigh + * ink → onBackground / onSurface + * ink2 → onSurfaceVariant + * outline → outline / outlineVariant + * primary → primary (foreground = bg in light, ink in dark for contrast) + * tintP → primaryContainer + * danger → error + * dangerT → errorContainer + * success* → exposed via LocalStatusColors only (M3 has no success slot) + */ +fun toLightColorScheme(p: Tokens.PaletteColors): ColorScheme = lightColorScheme( + primary = p.primary, + onPrimary = Color.White, + primaryContainer = p.tintP, + onPrimaryContainer = p.ink, + secondary = p.primary, + onSecondary = Color.White, + secondaryContainer = p.tintP, + onSecondaryContainer = p.ink, + tertiary = p.success, + onTertiary = Color.White, + tertiaryContainer = p.successT, + onTertiaryContainer = p.ink, + error = p.danger, + onError = Color.White, + errorContainer = p.dangerT, + onErrorContainer = p.ink, + background = p.bg, + onBackground = p.ink, + surface = p.surface, + onSurface = p.ink, + surfaceVariant = p.surface2, + onSurfaceVariant = p.ink2, + outline = p.outline, + outlineVariant = p.outline, + scrim = Color.Black, + inverseSurface = p.ink, + inverseOnSurface = p.bg, + inversePrimary = p.tintP, + surfaceTint = p.primary, + surfaceBright = p.surface, + surfaceDim = p.bg, + surfaceContainerLowest = p.surface, + surfaceContainerLow = p.surface, + surfaceContainer = p.surface2, + surfaceContainerHigh = p.surface2, + surfaceContainerHighest = p.surface2, +) + +fun toDarkColorScheme(p: Tokens.PaletteColors): ColorScheme = darkColorScheme( + primary = p.primary, + onPrimary = p.ink, + primaryContainer = p.tintP, + onPrimaryContainer = p.ink, + secondary = p.primary, + onSecondary = p.ink, + secondaryContainer = p.tintP, + onSecondaryContainer = p.ink, + tertiary = p.success, + onTertiary = p.ink, + tertiaryContainer = p.successT, + onTertiaryContainer = p.ink, + error = p.danger, + onError = Color.White, + errorContainer = p.dangerT, + onErrorContainer = p.ink, + background = p.bg, + onBackground = p.ink, + surface = p.surface, + onSurface = p.ink, + surfaceVariant = p.surface2, + onSurfaceVariant = p.ink2, + outline = p.outline, + outlineVariant = p.outline, + scrim = Color.Black, + inverseSurface = p.ink, + inverseOnSurface = p.bg, + inversePrimary = p.tintP, + surfaceTint = p.primary, + surfaceBright = p.surface2, + surfaceDim = p.bg, + surfaceContainerLowest = p.bg, + surfaceContainerLow = p.surface, + surfaceContainer = p.surface, + surfaceContainerHigh = p.surface2, + surfaceContainerHighest = p.surface2, +) + +/** Resolves a [Tokens.Palette] + [Tokens.Mode] to its M3 [ColorScheme]. */ +fun colorSchemeFor(palette: Tokens.Palette, mode: Tokens.Mode): ColorScheme { + val tokens = Tokens.palette(palette, mode) + return if (mode == Tokens.Mode.LIGHT) toLightColorScheme(tokens) else toDarkColorScheme(tokens) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt index 9ab773176..cd7c179cc 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt @@ -5,67 +5,40 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.AppTheme -import zed.rainxch.core.domain.model.AppTheme.AMBER -import zed.rainxch.core.domain.model.AppTheme.DYNAMIC -import zed.rainxch.core.domain.model.AppTheme.FOREST -import zed.rainxch.core.domain.model.AppTheme.OCEAN -import zed.rainxch.core.domain.model.AppTheme.PURPLE -import zed.rainxch.core.domain.model.AppTheme.SLATE -import zed.rainxch.core.presentation.theme.amberOrangeDark -import zed.rainxch.core.presentation.theme.amberOrangeLight -import zed.rainxch.core.presentation.theme.deepPurpleDark -import zed.rainxch.core.presentation.theme.deepPurpleLight -import zed.rainxch.core.presentation.theme.forestGreenDark -import zed.rainxch.core.presentation.theme.forestGreenLight -import zed.rainxch.core.presentation.theme.oceanBlueDark -import zed.rainxch.core.presentation.theme.oceanBlueLight -import zed.rainxch.core.presentation.theme.slateGrayDark -import zed.rainxch.core.presentation.theme.slateGrayLight -import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.core.presentation.theme.tokens.Tokens +import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.theme_cream +import zed.rainxch.githubstore.core.presentation.res.theme_forest +import zed.rainxch.githubstore.core.presentation.res.theme_nord +import zed.rainxch.githubstore.core.presentation.res.theme_plum -val AppTheme.lightScheme: ColorScheme? - get() = - when (this) { - DYNAMIC -> null - OCEAN -> oceanBlueLight - PURPLE -> deepPurpleLight - FOREST -> forestGreenLight - SLATE -> slateGrayLight - AMBER -> amberOrangeLight - } +fun AppTheme.toTokenPalette(): Tokens.Palette = when (this) { + AppTheme.NORD -> Tokens.Palette.NORD + AppTheme.CREAM -> Tokens.Palette.CREAM + AppTheme.FOREST -> Tokens.Palette.FOREST + AppTheme.PLUM -> Tokens.Palette.PLUM +} -val AppTheme.darkScheme: ColorScheme? - get() = - when (this) { - DYNAMIC -> null - OCEAN -> oceanBlueDark - PURPLE -> deepPurpleDark - FOREST -> forestGreenDark - SLATE -> slateGrayDark - AMBER -> amberOrangeDark - } +val AppTheme.lightScheme: ColorScheme + get() = colorSchemeFor(toTokenPalette(), Tokens.Mode.LIGHT) -val AppTheme.primaryColor: Color? - get() = - when (this) { - DYNAMIC -> null - OCEAN -> Color(0xFF2A638A) - PURPLE -> Color(0xFF6750A4) - FOREST -> Color(0xFF356859) - SLATE -> Color(0xFF535E6C) - AMBER -> Color(0xFF8B5000) - } +val AppTheme.darkScheme: ColorScheme + get() = colorSchemeFor(toTokenPalette(), Tokens.Mode.DARK) + +val AppTheme.amoledScheme: ColorScheme + get() = colorSchemeFor(toTokenPalette(), Tokens.Mode.AMOLED) + +val AppTheme.primaryColor: Color + get() = Tokens.palette(toTokenPalette(), Tokens.Mode.LIGHT).primary val AppTheme.displayName: String @Composable - get() = - stringResource( - when (this) { - DYNAMIC -> Res.string.theme_dynamic - OCEAN -> Res.string.theme_ocean - PURPLE -> Res.string.theme_purple - FOREST -> Res.string.theme_forest - SLATE -> Res.string.theme_slate - AMBER -> Res.string.theme_amber - }, - ) + get() = stringResource( + when (this) { + AppTheme.NORD -> Res.string.theme_nord + AppTheme.CREAM -> Res.string.theme_cream + AppTheme.FOREST -> Res.string.theme_forest + AppTheme.PLUM -> Res.string.theme_plum + }, + ) diff --git a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/Theme.jvm.kt b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/Theme.jvm.kt deleted file mode 100644 index 2daa82905..000000000 --- a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/Theme.jvm.kt +++ /dev/null @@ -1,9 +0,0 @@ -package zed.rainxch.core.presentation.theme - -import androidx.compose.material3.ColorScheme -import androidx.compose.runtime.Composable - -actual fun isDynamicColorAvailable(): Boolean = false - -@Composable -actual fun getDynamicColorScheme(darkTheme: Boolean): ColorScheme? = null diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 3a25d49a8..6b38f63a2 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -13,7 +13,7 @@ import zed.rainxch.core.domain.model.TranslationProvider import zed.rainxch.tweaks.presentation.model.ProxyScopeFormState data class TweaksState( - val selectedThemeColor: AppTheme = AppTheme.OCEAN, + val selectedThemeColor: AppTheme = AppTheme.NORD, val selectedFontTheme: FontTheme = FontTheme.CUSTOM, val isAmoledThemeEnabled: Boolean = false, val isDarkTheme: Boolean? = null, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index e713bb8a3..b4e50764f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -52,7 +51,6 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.components.ExpressiveCard -import zed.rainxch.core.presentation.theme.isDynamicColorAvailable import zed.rainxch.core.presentation.utils.displayName import zed.rainxch.core.presentation.utils.primaryColor import zed.rainxch.githubstore.core.presentation.res.* @@ -268,14 +266,7 @@ private fun ThemeColorCard( horizontalArrangement = Arrangement.spacedBy(20.dp), verticalAlignment = Alignment.CenterVertically, ) { - val availableThemes = - if (isDynamicColorAvailable()) { - AppTheme.entries - } else { - AppTheme.entries.filter { it != AppTheme.DYNAMIC } - } - - items(availableThemes) { theme -> + items(AppTheme.entries) { theme -> ThemeColorOption( theme = theme, isSelected = selectedThemeColor == theme, @@ -320,21 +311,7 @@ private fun ThemeColorOption( CircleShape }, ).background( - color = theme.primaryColor ?: MaterialTheme.colorScheme.primary, - ).then( - if (theme == AppTheme.DYNAMIC) { - Modifier.border( - 2.dp, - MaterialTheme.colorScheme.outline, - if (isSelected) { - MaterialShapes.Cookie9Sided.toShape() - } else { - CircleShape - }, - ) - } else { - Modifier - }, + color = theme.primaryColor, ), contentAlignment = Alignment.Center, ) { From d23f8df7e7203079fe8c2d034643d74795db5849 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:12:58 +0500 Subject: [PATCH 005/172] feat(theme): swap fonts to Fraunces + Inter Tight + JBM --- .../composeResources/font/fraunces.ttf | Bin 0 -> 360440 bytes .../composeResources/font/fraunces_italic.ttf | Bin 0 -> 414904 bytes .../composeResources/font/inter_black.ttf | Bin 344764 -> 0 bytes .../composeResources/font/inter_bold.ttf | Bin 344032 -> 0 bytes .../composeResources/font/inter_light.ttf | Bin 343440 -> 0 bytes .../composeResources/font/inter_medium.ttf | Bin 342936 -> 0 bytes .../composeResources/font/inter_regular.ttf | Bin 342732 -> 0 bytes .../composeResources/font/inter_semi_bold.ttf | Bin 343640 -> 0 bytes .../composeResources/font/inter_tight.ttf | Bin 0 -> 581588 bytes .../composeResources/font/jetbrains_mono.ttf | Bin 0 -> 187208 bytes .../font/jetbrains_mono_bold.ttf | Bin 277828 -> 0 bytes .../font/jetbrains_mono_light.ttf | Bin 276452 -> 0 bytes .../font/jetbrains_mono_medium.ttf | Bin 273860 -> 0 bytes .../font/jetbrains_mono_regular.ttf | Bin 273900 -> 0 bytes .../font/jetbrains_mono_semi_bold.ttf | Bin 277092 -> 0 bytes .../rainxch/core/presentation/theme/Type.kt | 147 ++++++++++++------ 16 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 core/presentation/src/commonMain/composeResources/font/fraunces.ttf create mode 100644 core/presentation/src/commonMain/composeResources/font/fraunces_italic.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_black.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_bold.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_light.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_medium.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_regular.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/inter_semi_bold.ttf create mode 100644 core/presentation/src/commonMain/composeResources/font/inter_tight.ttf create mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono_bold.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono_light.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono_medium.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono_regular.ttf delete mode 100644 core/presentation/src/commonMain/composeResources/font/jetbrains_mono_semi_bold.ttf diff --git a/core/presentation/src/commonMain/composeResources/font/fraunces.ttf b/core/presentation/src/commonMain/composeResources/font/fraunces.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8210f9488d3c732359a9292dd09aca3f2bae830e GIT binary patch literal 360440 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

_b5`Ec9fq?7KrG-p>D_) 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�Iws~)DL`(b;xPk| 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≶+b&=jtW#6``O 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 z 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 z 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�$8TrO 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 h&{>1#> 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 z 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 diff --git a/core/presentation/src/commonMain/composeResources/font/inter_bold.ttf b/core/presentation/src/commonMain/composeResources/font/inter_bold.ttf deleted file mode 100644 index e974d96fc818cdefd0a3cd06f02e13508dcb3b66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344032 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 diff --git a/core/presentation/src/commonMain/composeResources/font/inter_light.ttf b/core/presentation/src/commonMain/composeResources/font/inter_light.ttf deleted file mode 100644 index 1a2a6f252d75104b822141ee84d2d7f89edd0944..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 343440 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∾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�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 z%0A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

(ooqSGyr+~wg7v7X~!Ms4b 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

rife;5RpkCf%7@_EqLdp!t}|B70o-Azq!r|v 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 z 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

ERBva=-o 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ૂ`3XTd_s$gG`pq+cNfzhxxM*6D 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 Duw-^W diff --git a/core/presentation/src/commonMain/composeResources/font/inter_semi_bold.ttf b/core/presentation/src/commonMain/composeResources/font/inter_semi_bold.ttf deleted file mode 100644 index ceb8576abcbf1ef3270f6531f8f1daaf3f974f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 343640 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 z%0A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 diff --git a/core/presentation/src/commonMain/composeResources/font/inter_tight.ttf b/core/presentation/src/commonMain/composeResources/font/inter_tight.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7d64212163dc15e7f3de5487672668dbde589365 GIT binary patch literal 581588 zcmcef3HX*n+yBp*xt6c1gA*A;}h!goJF_vSmy5 ziH0#jr z(DvUqsk)PdW-Ua@&FRp!%hA`qTA@_JzMqNvxMzo>k8QhP|Le9&ykjeoW7~A>esH&) zK6~OTi3g1psc~VKqnkEA{;KENin#gY+V|*V4nAg6;r$aOp16hU`6mxOX~d#ej=o=H z_vIozYYZH7(x~Z&pE_IOiyqq6M{`|g(9qFiKYXrg9}!<&r2ZR2 zhM#;=`5~8|Eiy63_2Qu?jU7?Gl8Z<`o%E%{P8vEO9`j)$e&K_>e&LAWr;i>|@y!|H zU!ze;y`xg_8030}v;3%6Dg*{SL;t2Q;LdUi2hAIkoe`%z=~?^LNEBZ_a@cGKP*MIX4U zBo>MHir(bb3{(H6O!2q;kyK=v_(By~Q6R1ywv}#2A9=Jal+r@M%8>ieU(r`w_Ds|} zeus3WQURk3jB^tGXB0O6XX*cY{^=Bnwh_H26St96kmgzV6PXx~l1~4WH@MeVCYGm> z;=YBO4GnKY<1C!amgX9c=1RwM$H+t_8uaf_c5xJ zp|Jc<=_Yf%%*~Ub=*_TD2^*!35`9E@{{l{Faiw&$YJ}0Kv~aWOw(D(Lm>kdfJ-j7( zNhbc8_%7PzX3CEKE%OSGrIr42Z@zR&CSe=&|CCX3(BHi;olcuSLLH05-{Q-2Wo(vq z3HKIo@897`X&3#Q;7=2y=XuXRLH#YeH&Cj?JqpXSd`TN$fi3^9(DwQ3yBKq#xs+$F zwA(h8r0pgAGpIxEch)nW43stz`gg?bNxSXdEbRU-NIsDK?U*QfWfHRkiF5X|HOiws~6(|0(YlgjmO;|lz!y1E&NZE^i7I%{8PAzKF4+Us26S0t1!`2 zh7g_v=W*RBi|Z;w@w1`D`@`ZY(jZzV4btW^%v~&aFZ@dixBu(=|Kj<%_`zTDPD&Z?igDWR+rod=^ak?$bDx}x z|Ba)qEzbJa0%_%E$gm=<(dvFiVNUV7BU;N>;eLXj&XB{nHv+aYMz!?o3R}Y>Y5nhl zPfqk-%4*+LB7Md^U--^XlFHjiYW)$`xG+ku3$JavALR-gd2f~Mp4NG;)F7Xh{tA0e z`xy61vv$$8nNnE%dswfHWBw*?+r9r>)em|xm+6bG=Q1zS@ma^8zqD6WRjMT)NlQh? zyv$~sq%&>TOVi`4-LUIKO~?IVjB~@{LG+7OTIa$>#{LbY)$ySr@6mClNmdWOm;bFk zng@`kN0y(yyYzpCd?)ctgKcbk_HXqrd&i&eg$>l_FGN%R6t`a5>3wX|+0VnLlr~yN zY2)vbivD@2BwMAvlfpdzo>X#gNHhNt+=UIM;%j^pY5(SEFZy&9zeJjCOD~&A_lY#i zo|zz(q6+p*_N>TxZlPSovpw8G=A&a}aJd|Q+e&)C!P)nGCExh>V86dY`ssY@r`b1u z+Z;Y^$(&-o4EpQbG+M;-%!}h=7}J^4W^;0#?+lblIuF$Ki_Q(AndArYhU_{QcbBQW zV`ADG29vHY`LO0m2-9}Wa~%ttaylQ;dEk1U*}@oWbNcIbPLW(I&C~hZ-^3ianlw)y z)8`BKr>)Srg^kQ3I+9N_>U|aMdt|KZDJ@qk$^hk9KmA^(Ql44P{pp1-&u!F4CJ2jqG?#ea}f2A^6@N`ewy`lRUKEgZ?B|p7W-gaIr|Pbh*~l3 zPUqXx5GE9#t3)5yJU(OX>!nGun7nm7U0+y7KVeMBzN7k$eN4aM*t7=u*U7>W&vh&; zC$1go*Ti$B7e3ZCy;k~#V+u=R#`LUQqv@xR>n4BHNyovqyt_5!oP+k3VW|)|mw9Nh z{M4aZhR$I-lWrR2*E%qth)2+tpx zZFN4Rwl#hqGqvv9?3bzLhn>O^h0)u_CbX5-pKnU`?yL_N`|q~-c{W#P?yi2LzL@n( zd@l1n`Vx86%6zKWH>rR3tZ(W(eir%TOTYIM_0@V(J((zdeZ|7Z@zTQcNdw{*Ft=eH zovkmJv(RVTWIn6DI*@CPn=45?u&@rT6Mf6Padu&TrlcWmtoA7_6X9AKhJ&(oMAkpH zE{Jo598`;QTQ{JSVFD25{eR0pd1i27b=<44I@wZKlRSvdhL`ajWiox1ew;kbSkp^d zM*WMk!I;MlyI_!9Vcv>OdyYck(RNzxo7;2hA`GK?nG6f1!MQk(k#pY!hW;N zz;?PPF*&JMxHcJ5Se?!T(#WY`dF&SUDHeOrU!XAbs^(0Aq!>ff4()|kW zCC4!K9K@QaI2I+oFgEE}cq6^GaCX{}JoYP$NqZCba$!ue2-R?{!cuMX|0!*@PFsi% z;P1tLP%Xlb19i^ker*`v7BY|5_U%j`)Be^r`{s7b`WNCBI;cV{DwQ57?2hH%imZ0w) z1>G6nH7{Lfw#(w^<2t7B?W-IyW)a^lYF-$Vy-VNszlql6U!n7<|0S?>FKC6WC%{?j8V{KL~5P|8K#(=@cdECS5)A zCx0z-DCSO4Z<{~))hP2PH@mP2|7eoU%Qi@B=4zGv2Ikq!(cI0<|JM@c+3Y#yYWkc$ z^T&Km>gbpo#xnl4V7zz{WvoJ@+<1w~w5o^Uy@bSjk8t)UvbNU{0pBiW# zo}G}CGXErw`OGl=K4^J$KB#j!%F;0!Mf?=r$($=q zTv3zeTr-!3bHn|V?S5&g_rgM19A>d!#-4W66aVkYy#7g<2c6W1paFKTL3z##3tdFs zLyOPv&D?Z7*ZpbRQpUxJX)pHJ=CUUBa-2UKKLeg=!k95a^I*@c7Mw?(?ev|*RpQxu zcz1WbPq;_u%wEg$v|Uc}F3;GUlg43dVX?z+^xBV;oBv$yZDrQ7g(KLP0kvIb$3=z3 zs;t2_DWOeaQN~}=xH+sX=45+0_t1Zu(XM@=6XAK#>3_Vx6*~_51IP6?viLfz8yjKY z#?piGXSjZ>C+{MDr4hQxY@V5|_V)An-mo^rwhw~tCsx+=4XW+YtcZsDliw(4LprZv zr*;GG^@DDD585iEY39edu0h{p?`lcf9Pu^&4A1F1mFk)&{9f)qg*&l7^df0m<{Ha{ zbgN8OvhX4~B~|7XwkC7s{$#G}t=DmN&UhBcuKyRL-%7^-AOwPqK?&c{36^VcMN{sR_3QyNG0aU z_vCJpZp_=eMISN#ZIzb#zH}|;A3bFpOy*3ZyT&n(&q!y>tE|ap>{L}|P=>2C56U%> z>&2|2%IFJ&;>J=-??tnjyH8-wIZm38XC>a#oi$>l_S5?OJLB5_k2FaY8~@b@b&S__ zMmE;#e0LDzuC8mxVMm>N4`klbjq^Ij{pc>{(;c(8LD3u1RqK|FX3p|zVQcOR@*0Bg zu&%_eUFq}L*+#kN>GQUis$+@nXOw5X)|~O7Jl~=kX{*AP$kRVo%2b&BC*~1WZvGq2 zByLPLzVxN7t5E)3r3wB!jxtT=J^HR1**uVW7wB3o`Ze(Q#Ej=$wTmD_isS;J=M9l5$y`w%A@a8dzV^yetnQI_EJ0kS`F3$ zWz%tmGh9nkh^2AbD$SPT)*u0x6cbv-(j&pCh89>4WK_1)A({p2^& zZfR%l5#F1%Z$P?n@=d9Bdxpj`-v_ z*n_wOG(F+&MS7r}!wA-_I%fmThdDzl!VP$5Z=GutX{Bp(#?dCs*(Wfr)?%(a5ykeu zE}{RA)v+ViGgxJ5QMXK^oD_d zTnu+YJ3}|nH0@PcpD4OcQ923d1=+s;92w~56xOmv z{e7+a`#NXy5SPwjcPcLBo^(toya|l0z4r4CA5PgLE|9-l5 zq;2Ql!S}~A=O54d<^SMW`q6OOdLn(Oq01FkGcNc)*wUQ-r+x2lzb)L`_B~;2_^mQy zhK~D(*?6+06M9UfdnV88axxp!TB3S(qGOS!ZO(H;G>$Ym)*p?+`oc!8>w~7%HNaqW za#8E^-P85Q2%g)XcpWD-t3TrtXu1y2H8aoB zW)h%|g{v&*iunlFLCTA89aUM#%6}M2|bT9bteIevRg^&*iheJcZFg%zY56Y+tfdNzIb_B?p!qT5@E`aV5u>oKSLN$$2H0lsr=M zV#&KDpO$=IvY}*a>2{^{OZ$|LEIq6A{L)KHuPD8?bY|(ZrAtazm#!;wWuYu7D_6E- z+0JFP%j%UiDr-{KylnrnBg?v!9bMMDtWVj6Ws}NoD7&ZZp|VHIo+?{h_D0&((dU?mz0jRd;3G@9Rmuq+aWKC)FEJZ&v*s>+f8@d;R|P zC)NMH{^mwWqa7R7Xnbzt^YQzOc2y);+cs z3i``TcE)8tndLgUm;AaQp?s(q%0k=FKAapzgtNm$?0rSJI@}iS2@i$W!@6j{%Ztq3dyKkMxu=gzN zJ+JO#b)T%euU;o|S@qe@TcJ;+ z{)FpPqIJ>Q=-Oya)WKq90eX|nq(?C0VBWM3T&bb0TQ{{@eXz*t16H?Qz5nV~s~Oo= zty%T;st;GaxaM+^uUCky86&bXx1!HV(yvTcbYJOKbY5}fiX%i;WO-;l|Mky`(cEKq zPGq@0B&#N`x^Tq{D?TCi{S_~0%vU9Z?^)h)#ZxQpS#kIB^Ov8qeB|=I^v;SmR=l#} zA_3eERDXH`o)z?+kJ7!lJ-k> z{Njqw&K3D|$p?45_r)i7eiTk>dBXm^-feJKgF72s*Wi){=QKEbw;WXUB;V}TrxbVk>=BG$*akTq-%0iQYqa(iIRhokkn4%BuU$* z2c`!n<&#(V29!yCM*B9>TTYT8GFm3dWVu>ymf7-vyeA*ar?NtRkgYE7YP*JR57#O= zCaIGgn;w>Q%#C-0+!S}6yTv`^K6A_51|NJiU(?s~t^9$$vmfox_UHL2ex`rO|KK+= zu7$9D(k*#6IXdkS|C*FbcTJn5HPii)_mbCh*Bv8@bDq#p$8(%6L`o zg0yS$T2di*MZ6-kP6oyA#&5^(CELkPY+{#}nzDoJDx7LbXE{&~k;CPD86#)OIkKnR zM?Zc@9+k!Nbb5d+auuZDyo+5^`N&`I&UL4{k?s;V)=hAiy6NsYH_tuh7Whg&_T~I` zZk9j7_wrr*(SC&QnYQu6{6as^KjxnZkv}UW?2Dhzum18#+!$r3+aKKJL#N`JY0F3RE9ECYOMdh>%X)v4 z+s&_a)%@#D{3C8x|DCJqUv<^}H?FSVdt0huZ{`sQ+*-0M3?IX6Pa`kXxCyE*3{b>;m6m-r`L&OhzS`Dd6XRdHuY zRdZ{pke+x*V{ zL;sQM>W=as%eAhX|HOalj`p9qW8AT>yF1Q*?t1to@`*p!9q+$zJ^fNQ$z9|wc31nM z?nZZyyUV@l-gh6kSKVvwAMSO(oBPIn?fST{+~uy|esf!V68*!SiYY^{!D+VpX@ISyN2ETMgC$pzzy^>+#+AYz3txe5BmH4!~R?UqyNco z4VBy}VV6+Veds=R-?|^%I(M_X)-Q8!xW4{NzpJn7`uTI*ckX+4znkkG^bhzu{Vi^Z z`@(F?U1m>fANCsmX-og5K;-AJSja zpVFVBF43x}YqUBUotzOJ6@49_#hl@r=v#gi`F#3G`f1cYE<_!oj?rP!;n5M%lIX~& zbF@5K5v`0qi!F&q{~GA}!BU zJQ^MgkB9kTL3lPiA6^JAhL^&t;l1#F_%M7BJ_;X)&%x8-in-4W(R4U^N;#p&DWJL$XWd+DO|ts%#EK6?ZVTnNuFUn@^cs;HiZkqFJVI194-vMGGpEnE(*Vei^JA%NhpL#k%UVl z7bZtOTo#2eC5po3Q5>#_l5k~|hO44nxH>8qrbgw%HR0N*Lbxuf7^X#)!u8SiVR}?K z+z{;$W<*t@?ZS=Gj$vk0HQW^K6mE{Hgp)!xHZ}(%#L;qw?(^!+oKxcj;Lms z6XnC5QAxNfDh+o>W#OKvR=78+9p*-L!hKQQa7cJ6+#l5o4@C9DgHeO6^JBxuLmXx#66)O_W;hN~z7?2CM6) zN+WlJ?BniWr9MaYb9YM%H(U00x5?4&Wlp4Cl0)5mPAK1Hwg0H}ai7azx1N)}&2qZ? zRYtmB*g^Q2UrBG2OZ-l9uCE}M`Mg}=%lOq~9l60blNo+bnd$eEX}+=C;`fzVem}X@ z?=N@xL*;JYUgr8vGRL=-xBW+*Iv4!ou7>~K?d<>IYWWSW z%&&L5`z@}H|Jl{^zqtB-vuoggb&Z2}je>K{L+V&-p&`f_d+*v{<}D!LQ=D%Z=eb|?C;S#MLU*vBD`;y6yCe$mNM|7buoFghg~6rCEKAB~SL zh$ciAMiZlpqKo5w;#Tp#@qY3CaqIYixJ@)Qx`vg{bh^~yTimr}_$0OpA@u>Lp zcyxS5JSKWFdMbK4dL~*BJsT~Io{P)J+r<^*3UQ^lT)cBMBpMnGj!uh)rJu)R)6dd1 z>8iMK+$i27ZWyUU3_SKaNItc$!)Tlf0e0lMGExO9m#xlB&t6$>5|c**V!Y$tU%b zYROK?_DS{lt)xn_V^W$lNOn&eCM8MbWQU|tQY&ek?2#UuG)bB!&5}KnAxZOOucSq? zchWN1CutS0jhBbU>7aCA+CS-@92c6Vr=$bYlatO#7xr;V0>35+HPf9@&S$*0N3$(zYt$=YOHvOZaq{E~c;yzJM+kH_=lC*mj5;pwUAY3a~(WI8Gx zkq%CWq{EWc$*T15XwS5B+B$8Q9+Ip~mM32(Ymyb|tn{|@&h++-Thlwz+3B2&>FEvW zjp=pCx5oZ96Xk8=ihSoE5oyuA%BVo@{bY-_YE7q=PLnEn#HZs!vQC$T>5}|t-$w6on zBME3zBhj)ngMIlsy-%UKwj_@vMq3+cTl4@UJrLDZDx~a4u=>eJSU<}_1}i)fy{9

AUZZ}uaU1pkl|jYQw0c7arF(8oyBHhqoUcvS5LZV=kfa8uBe4W~O${f+Ez zVGke-?jdxb;XXr8G2Aj#^8&X4J=O359c=h&=xK(piE2B6S6?1#c&+0w!>jKMH@vpV z2*Zy?M;iWYbd=%GLr*un)oY50xkSVN1p-)z=|+Ir^Z%um6Qzi;VP8 zbRIm+n6MIk#K>KMK5C@ePaZRp*U-le{>D82=7Pa5WJI1Y;uYwVMre&bWhClTPaCn? z`57aA8(m<;YLjOTc3?yn7TF1Xu82pUFH#tdAUd)eWgf4^wlEj zlUg@LeO22-IRvx~l*2)7s+=pUUa54J1!ETkvk|LVl7lywc zU23><(Pf4^75&n1Bhjx6cL}=OaN7UW55P@8)wjT1imo!;bab`hv~9mOociM$!>QkW zV>q?dw}!8TerI^C%UZ*iL%%otcIZ08%|d@L{1NDS!}mgeG<+9ygW-=xe=__Cbfe*W zqCXpH8+4PwDK0;#KzVaTQE-iBA3 zYWsm)j_zZ4wM{F7y<>6v8gdP~pTT~zxcv=LKWS~S$1LsuL#{*VOA7nW;tn)qI(m@7 zUbHxEOW@p0Tw8o@b`n`4S46n}}YUnrCwKu$`?O^CP*>yC$=H1EA@25N5V82_O z`W)!D!s#`z2QIF&q2Er|#b94tTvtQCIZm&^_eZ-KI@Y_R4ffCNSMvC;j?L~^!;eF| z8#*?-;|xC@?P2Kn?T$Ctdl%Q!$i~?d2m|}^;(8f6j=B>K_UOg+Hgv3YeGK;P#q~9G zY<4FZ?B$E=XXv=-PB#23RO<#hj=BK`d;a1E8amcGwHJ78yFo>?U!H34w>*TC4x<`+ zn&DqZhZNxz?3HBjYZ-n^P(=IsaKnFxYTr||AB{Bl#f`B4VpK=f?%>rXql;*NJ;U&u z&@n}{4{3jc5Tj#@?1`RLM1B8kBUDDuDRKaMt`T-Z&nvP&s%;FR3aae~{zi0skt@&( z3|%+scQpH!)`9$l%OG_(fKR&QtW;oPD30pp%MdTWCKA&Q>@>HFSN* z$wh{)4|Oe(p?Ru5DR+XF55Lg$4SRzbx;Ap^Lm++8sYar<*Rq1HC)~9~O3>>J&T)iO zF5z@bIMK=e&8%#WpuTs5;cKHa48J?7_5$Akomu1{RO<#hcXsNFz^PF7uTW)cpm~I6 z8L|+))!@uX*w4(+`7-;O8GZnIyP@+h_CGVc`of$dYFo7n=y%@TWpMH&?2~5bdeLc_ zl*OPn1+|l8~Sb$hrVZcwdMOo)V3cO{L)XH`ihdkM}}8B zeq2P~r8Whxw*0h+mh&^it6e`YqVHT{NEP&pB4?pX4XKJQD{>C{r6KAMUlq9wU2f<; z0jI@=#;r86J%Uw)6^&bM$Q1NzgHuhL53P}GjpV-J8aU|`_pPDpCHI}-hoI^cp!*%} z`yy(abp~gn;(jo6zl!sN4Bex0KNivcvBA(i4^FoYt?NeE1P8+}2B)jS8AOKchW=XQ zLUfBET9@C7Oh&gFx}J0DyTHk;c#5t_WfVV9^xL50mm)i(!O-zt=Ua;IUFljy(Z1sG z2}SpzbPkuz<6c1V2}SoQbUl-SUvUDQAs3+A711(OFm#>GxqxvzTFKD$E@uYDKvbUr zUFYeXFGKUhpA=os>D)2n5)^;R=A>GWor=sys~I{+_gZJ=HK6?zU90KbG23mrc`n1kLvw3I-w005N&et(?uIK9- zLCadtaEGDwi!4SP7&;%+Z%hW`fp2K&+)(GK8SkKt4V@$Eyf))qw26_;7n>4R=)b<1 zq4P(-XA#B^9m}%utP8qV5yleVqKM|VcM-K=%Oba-`xMc7wSs-&L)Z`Y$DUnL`cKBk z=mCaYi?%UbH}pU_2tI*>4V|m{wnaWe4*_iNjzQJd%IBc=1)ZOIwW+cM)Q+Hg5?*bm zoC|6%&^fHmvoq8u)h?j(SzVK7EJd{rpz{}w6u`Ia?Fm8_;hKXKWezZPEEbHb;0J?Nda3xv!CpeJ2qH z9V2}|L+96=!)53gtm}bnO>j9nz{tiI?Vq6IkghSZb;cHSP>}>Z)rhn_gAJXh>zJFN z{xYOULsV^{sP7CbqW&@*Mu7Ul$Re%LQAM=1MG^9gY!Y#aXef=-PIlwip)SSH1xa8nXXX-)n`Dz)&62bzvupv zA`hbKE6V+#?^Yg$$wj_JFDvpRI;F@@sJ4x=6|OKsCG<)oThClY7{V^7`X7X<=u|_$ z{r(z5zxn=JL%)rjNoVM{jx*^D{RZm3o0830rxOO9FZ&ycEJM{UpmQ8uGi2)q^}(4% zzC>>-qISNyNL}<6L)UYDRuOHRTMhkI^Gl)({XYBK44sGi+YOzka)zFf%|Yi7Rvv&m z|HNH{6@BO3a1Ui(g5C?7hxUd047U{3cPI`tFGWDx5<(;NAtUUL&NIRusJ1JFX6PeE zQ2&3_2o2E3j8Gqa+z8Fl`9^4gK4Ik4mQNZvZJ(!%oVMH3MxuS;86&4{w7|%zy`MGs zTespD!gC@yZQJMJMa~z_LSHfx?JF-EIraBfjNHZOt42=S{55!;e6%0@!^mBNsvkg3 z?fj;ZQ@>bb!rQ|$w+jW z5t^W18evaVeG9@~=yD^`wpd{#+NLXwgg)k18Hw6?wUMaZzBUqVyEX6)Wt)V43-pyl z+iR_noQr;MB;(L^P$11kltTc&PJcwbk#0n}q5BHyW;8L(QK%{(zP@(qGW+ zjPxh8f|34=R)jox{)U#p!IXJ>v@IM;Jna_RLqFo_FX3buPW(OS2qWr(VqYb~Her+z zbwy7%BJ31K!x_|3b&L@mg`R0d*e{%I#Al)B0KOAp%Ycm(sx1NkP@?Bi{5xQ?6n_a9 z8j7p;y3_gbzorGNL8u)o?5E_(`}8?k0R7s&&4X@R8_T zxSufo5*{!jd^0>~M9a{JfPNUgfWB@-_-puw5#g8N4I}yzr9Uds7w94*`U-u^h>k-S z!`q}$-=Gf$`dickrLP6rD$@4-(1_F@=wC|I4W)l6(RZlY3!;EXe8%#-%zDMxDVhHk>k1}>W$1717T2~j!2}0Q&IYm5(c3-RForp8d?s@ z6CQ|eXM|yB1tT1VRy0Cqw2~31SG2vM{UoXkJCe@BQFm{OQ8A0n<-w3_Y2CzG6`l1buFaT|2g#Kt_L;GvAhY<#&O`s{y4o928 zUW6Y-Tfp9g7oaU+U&8dSXg?#oitZ1G5dS*b4h|*!9;$T%^*iljSwH&#?Ffevul}cX zhRmN1Ck&aN9YGi}|2vW}1o}dx_U%G=6{o8dSi%sN zqTQhf;jhr+p(kPWV{J1CD^ayIWPKGs%Y5w>RBa96T~yluvOc4B2Oa04lZ>zi)wTy6 z^CGo7gl|x7576;1(sqLIEjrN9F)=#D2;ZTD3>_n*Q;o0|9c<`$8J%W?@6jPf7>f=y zwEsoJjBpmJ?G1sx8I3S>tc^w*VLht#0UdWEtpj9zSj!FJTvW>n+NUEeA7o>+mI<=H zt>pk6uOodoXkU-?edqHXnT3uw!aVeXA`Q_AMxyV(5H4YCUyM$IO9?MRCmR9(h%Ph2 z%jgs%Jd9pmq!D_B5gtLWEYcXg%Fu6JbhQzlM5h`-ef%0DJdIv!=yx%?&Ir$-(~Rt! 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

FESm3GsD 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

^Q2@p_nJptvCyhdNtfUDv9XvP^J$Tq^Yf}I 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 s3n!(`lCJDArN|NmBd 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

Zh7cd=34FaGWy`SDvi)66*nbC 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 RXt+-ctVzYaTEQjd{{RH8A$|Y= diff --git a/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_light.ttf b/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_light.ttf deleted file mode 100644 index 15f15a2a121f3544286a7ea5ba886c56552e99b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 276452 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�gzxH>XR@G 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 JS1YjO{15&)U={!X diff --git a/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_medium.ttf b/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_medium.ttf deleted file mode 100644 index 97671156df256e850498054fdebcd41d74a65d6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273860 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%0AmA2ptU z`zE>nAom61RSJES+%Lg>(f9@2?Hnrke}!+GO8G0ueTdu#kfUPb4#01bdk){Qmpl)G zUO@b3$UVn#rG#VB4(AV%`vh6z7`ca(Wr+TQgvC7UWC}4?y7(RxV!;JnA8)qe6hId7 zpH%!}yi6D0UXyMP++UD{nNoidW*2!E%>;i!>LcO=Ur+EynWE^DGyX2hMPv|9wX_EK zVHr^-OG<=fiHQ9&RQnC_TrKI^Bb4qm8LB->Fh)CYjutX6w@@mR$elv&NOG?sId74i zVU*TT17#sTLX?rRbi{2WZ8U|BBuz#dQcpoVVOcK7Dar*dNB_gJgdp%LYvRn{~ zS|q}hJ6qNbQ7P+*2+KNyP}CwZi|8?SCI4jv$7L-742rf|>Z7eTehA8Z3LQ%>z7GXT z9myO^GRMj~DVEEU63Z#HgmkVVi~WxBl`rk1y-Dy)$)kNt|1|i+#2+RJizzSHk~@WR zG?H?34e<<>^0l`JE~C_kQmk^)^KL4URg~h}`VS!aM&m5pk>m=7e+J4#(yf>@kz*#T zK_xy|)+

78rT{G~g#GM^93Ud$g=sb>@T)-N>tCtE|-mU6PsjY>muYf187 zNm`uXIKfYnTTAXp((^+kr;Ox}B=;LcnMm$PDupn?<;tbFVUigpI7ajs!DT8IyRuiQ z)%U=avj%}v7SbI_b{I?WSmGQ@C4w;n`OJ!{ElP3b>V42yT^C3AfrB3%ACqmzUXD6XadI*5rA(rOP@j zFZH*kkvoIj+2qb6w}adsa+i?19KR>(wD$D2ch0l!=?UyjdicQDB9X6 zfBs`_?!_gZ)>e7ZowZ#&hwtZ<;2Sw1d(m&@zQca;DtcZFQlt|_is*95pzT+>~%U2U#z*Lv3y*9y4nU8`Jc;cjwm&6@4n z;o9Y{b?tKG^O=(#zeo@I&dp{NWqLF7GfOhdGeeo1GOIIdGpA=x$eaRqdgkoRHqbU@ zc4schT#>mdb8Y5&U|TbHNbNIsW$uG}F!M;}(aht3PG+8hdoD}Ma%Fk5^0P|770Rm4 znw?dfH39CFtm$xPXSHQ@XRXRwlC=Wvs;sqH>$5gxZOz(|wJU31*1@b(Sx2&tW*yHu z3FuVTxoj=lmF>;W&o0R>hvuQ|YIkjRZT1AXQ?jRnGCR8s?vm^ku+pmRwQ$#GZ-Tou zdk5TI+555&W*-4`Jo{w!sqAy^U2e_oa(mtR?h>~wn~)n4-4onX+|%8&-EHn}_Y(IC z_bT^VaISZ6a&L9-0JP72(0#;x)P3B2(tXN(&ZBu;9&y3*_{x1DU$w8+H^Dc>H{Cbe*XHZ?E%B}Jt@5q)t@my6 zZFNu5H1|4nHqiaDJR9gKmuCY#ee!IeXP-P9=*^O61HIGb*+B0Wc{b21-z$Z{su$c{ z>C#a9X5 zOfFUjpvisYex2M+YWDoG4E8kk9()UeZo5#t{PXqNjwjc`zTzzMNyBg z1Z|^lzT!-#q7iiiVwhSQMu2S1i!V^-;mebS__AXOzGha2bDpE= zljP+v>Ax0ytat&wVJknz+=o+@x8h9F z3hj37)A%mr=Wrr$t+ozd6I`!tz}djB?0%I7yMSO`ECd%-|;ZgXya1`GYuE9yCaiR`i3vR-9f+vb=#1wo7cpAO{JY9SY z-}{}7Fa5UQE5B{{x^D-*=-VxN@EzYp_=fLNoRV5DR*022n|P;Kh41v6O3n<)I$bJ#i zOBf%@{xGJ?ISSJEl{3zAeIpf)6Brbpe}u#3`CiFi!uSR9JdQqY74G4DyG_PdtN7V( zvwu#}y>BQSt&QyB#Y|GB<2}PT+sW(jbA0bR9DZK$`w)lp)7f{@WxHgrQT!gi3eUcS znneR@8dsivEK+djZI`Z^2{Jb*?&vWeG z$Ki`OzC8Oc`Ij=jjPd0P&wgIvSyLE4#CQ|)&t`tMa~|uPw@HQPZBgNQTbUnkwv?U| ze*)tZIeZf1lNm=JNc>Y7NB>Cp4UA7`drE!w`X+n47gwJ{&-?df2=lf%0i?@_p)%h%82 zgQtbV=PBRiYvX1jC*ciockNk z48~`2IyWgljr(suw=aJ=%jNdu4>2B7{C?KUKbG-YmYdIV1B@3k&U$)E80UJK=9I_# z^C>&BULJ1u9&Y!Z3YCuUIVIP33!Qb(U;{bqTh=Rl`{yu3mN!zn~nD& z7ry13LEo&;#xEp2csKIlw_!P2E`HyXrw!Kf@t!mk??}V6;rNwP8Ghq*nf#3%epTs6 zGXK9NUHV_>6nG!4{u^1r|DmkH|DmiQ|3g_t|3g{D|3g_N|3g`qTuc@S3Y26wZ8-p8M%p^xy} z8b*4*X50<>T5T@e)y4+t>Mpo<=;?5m>rY8ny8-SJtR4tokKE_*_2?0}9lA`bLx(25 zc{=(B-#n}>2;XdDlXT_pe|+03uTtjD7ed<=z6xWpbg^b3e8aR>xJAbO(j|TJHS|M1zbKF{NysvK@HX^{{y5wt zI(Dsmn*2?%cdxz^?in5ZrMDY>hQOOI_BHX|i4=3DNY{J%-QNQqkA6br#NI_u=ADFo zMPSTFujP+vu%5S8`xM+JZ96C_;p41ybXW3COqe`a)I)Zvt7xLS+@v6Ll1gc z$uDQk`52+)Y&$R!{!8%RfjsDxHwX9;$ln0}AaKlhNUo&sMLg)^*#rKe@E-%-4Sy&2 z``~W_e-r$zz&qiig!Ag*qa^%o@R4TzWcZNnhrCOWj%PjK4o&yZ^y62jpz8Kvl%8PIsu`_{I#HsL+Gs(W2~kRI*z%V?vIeXFTlQt z?`azJB53D9Dhcy%RJ%unTIXe=B0`@?urce?QT%cj|js)7@+GHzNd(@gV849{L?aDeJy9 zpna2QcW8S4z3v5|K?}(H3glG-TjHJpd83K867ojv$xJqD@4ez{As zPl0xZXj4J+1Jix5whwj&UoC2<-#>V}OdL!M{plt`AowW?KEkyeqXwRb_y_1FfqwkP>p8;(RXs5Ggf_4Yd?gVW$ z(w&t1AZRN<%aeKYK)| zY}}5xuyf{;99Y&(cD65rE(<_w!`Q2PXG=aCBZ~V)(5C0WF7D@uHVL$CDD#fY37~Bv z+HlbBMJ;dB~~no($x{9V6OxC`*(?j-GJ<_FFG$Mz5*| zZaZ@kXm=0|BL>!)(Z^)Ap+&%c(61ac*v~gTa|URy5)C6h@lDP^9u%L^4;t)}(dL70 z+0rgErGDA_L7Scpo60`HoucPc+H#V1^6s7K&SlxZp@XmyKL!%kpR>KGg2)`_oUtMO#ANaI1>s zz)tE0(4<|)5+AIYeQ)|fxc5jt10$m3NZ*yc4z#tP-9UWz0$Y;~J+i5M4UD$Lu{s^H zvR6^uuY$G)*q!OXW#0yQ7;6KS+%F9!{6*!UWyu@t5AcKUSC zx`}TK_!fb0LUt!;9pJm4_)djFNfPgaWP+$9O?O3o!7HbpN-|< z)3a~Vbo@U303h@UdZs=F_5PS-`^&{Q8kLcrxehTzZdd!P5_r6N>tBJk?^pf;pp0S|I8&wY5RYczmy4N!@ zJs0$QL9df3L;gLQ;VH!Kxe59z;y)~B%XrT=vJSXTd0{*69Ymi<{3|uX^|Jen>p19( zK%d5P`aplq{i;jq-=^f)m5|d7`X28**FMl&m_7mYIiNr6+Tq><`VB-6ko;+&Z**;P zqh@+15Zz1kCeZJ4t#xk%eGJhXh+Yl)GS>>(6L?1wy@}`*pm(^s-OE5PBKj3XF9dz2 zYqlFD?DZ1;Mxwhl!#&eA$u$LZyh)=}9+31Bmu7kwVMlm1ETvX8}8ju(jfDu?{!&=-v~WY3sJjPPa2O-CEnWerK59%rACpLEfK85|i+pq(8J1AJE6JNyM>6gU4*i&7PwE}_BL0E3( z<89rLG%se!X_M*QT@&9X%6?Gj^o^#8?UIrfqw)ia|EO=E$WeDX$C4D(G2~g8yFiDJ zkSdaed9bWe=4RA$hpKxQcz1)|jR!t>Yls&u(nO2&&OkkOs7O$*EU8^2@g>QJd5(OP zHj09L;oVD7(3hZ|W@wLNpY};@m-dv{AigR#iu=UAs5_cATh&<(>d z4a=~NG{a@28yQBXc{!|jzvdC&6kEkZg7%}ux5akx9X%a4JWR$t5BKTl;jkj;(SNHS z*MDbZ8QF&0@EACAXZVdABi9&Y$-)TC(>|N#6;W{F-gBh z@74Qo!;+3Y)EwL?^9XLZcog?rJT9IPPvUNir^IgYU9m@ePwW-n7yHBy+t9Khx$8xJvfbU+&|j4+;`Hy-#^B8hrh}H zO3p<8R{w0@YTqe8`eLk}m8Mwi>C~DdzJeX)Ph(H{GuTy5*z$v{f;X))+yL|ioH@GN zUT1&NzQ_KOeXspxoIv`Dy}{mS--jEWHrZddH`@=`58B_vsicSOZT7e9hwX3M+wJez zkJx+c@7a6p@7w$AAK3ftr_pYPYL^+cM&18h?u*)oaIw>K_K$@dEf@p8xch&e#v$!; zssWo-9Y}4h;u~U%ctAY(pOyVZEU)U5td&lklC|?1)X$BwhWZa^S5TiNd$MP64&hn! zXP*>P^f&as>ZkO->8JI->u2;g^|Sh0u4ZunK}u%pL(_E&?}5l+grZ%>$t$rEB5+9WYjC;D(5T^$)-^rE8+M0vtil zM_~ee9&pU?;Np(8K^Xt!eh1zYqt&&IO=Jbm!dWBHfjxkKTDRshDF z-@g{Pq`^mP5`N4}2HgXHGoFX>Jb`B)o`L1D=j&4xSD?i}0+#vlD`HWy4G;2O z0v!VZJf(P0>H*|et_B5Ao&n@D(1~X$o;&fNyaLE$0C58-r@($Zhw&hPffIO6<2kQZ znSy!nhvFHHXAGVuJX7)9ga`9M)bMkL9JwU)g8myoX6~-s1Hn1LZa~lEJ|FB0t_1W# z-gCh_gZBV>G0z=bAAAtd(cD*q+k(3QoydD8xF>iB(8=60!NWn^`;>b&?+|XB!fd4Y zosz7Q!V=7UN+P9ON}qr~rgUHF^RTpTG*S!DjVAa;@L=%8k_gd9Qw!6L(FSH!xliOR z$FF`Q|4i#K;5&2oA~lIOQ9II&E7_mm;AnkD?&iFkf~bKq9j0Glj6*sP7M(4IwLp)d zH5s{g=Gj5iR^XRWyp?&U@vAc6W>DhAyNeH%#x$dJTpc^A(c;2GCl_(8LkK^5_e6xVknLgxb zF@+l=sISnCkc^jeeo%dIGReJ6(ZT;X=%@1n!SRX?KAEp8ly4Sr`ZvNif)zt17Z0U$ z3zR-)A$lGoXYZiX!4jee3Ew?vD1Lb><=ja46FK{XsJ*byP+1<}-xdt8oJ)c4&UprR z7D)adO8e#PAGBH4M8c77&fc6u!Cc}WB*O>YG-xGm)sXTANxgEOK>2!!KSuF)4w{3z zzhwLvg>TQ@hF^b5{0fTihaEB)m+?2`JSf*(5k3a>$jI58vmL)LRrr;_x8*z$G#PII zzBAVjq6L6IB+tiFyqTasnClOqrvNU`%}Y72;hr;zk0AX1+?|0lil52@>B`ne@zGnM zRh)i@4%XzSYX4D z7lyn7`l~}u4-X8l7(NE@nPA)SalIxf)9@3+&z9PnS(;HA81eSVEX^z$ zS2U@#q%?$^-Xi|z0xuO!0Tc@^&)HBk4WZME=9LaDjR0Cuw4}7UbOJ($21fh06fFa7 zMbVnl@uiY)UD2k}8KnyVZ7JGUw6hewPoSdciGaW80HEAJiT~cBg9v@D=%vy*r9FU- z6}?`%q;wT-mh%SOIU|csfzOM3w?-D70p$0`0!2mV!1vB2n@YP&S3u`Gi?z~wOVM+c zK3r^;?kRl^e7b)^U`MeFkQvzNKT(Vv1#Ca!7kdHa`kxQnU!03REWfz4bX)0eKozC? z0$HU;0FA!nVCjpccyB6>6xWu%UW%LUipLjEEz`^VfNr?NU6x-~4k_hMxsDfgZDL zV%dxeyjvG9FWFT#2R-P~k`rYMfG;avUDgACZSnnOOW|)R-dVO1{;o?#mLY!e-jY>i z>);@GW2Fur0%#o7v-D=5jueLYhOW|kc*dtI~2 zUN4_nzM$fsiY=OTSzg&2C3eNuid~vj;_|I5m|U{F>=fu5M`acCT(+%Z2l!8n(njql zm{;)_;FA@*%Fb3CtT+nzT*b@9uU5QX@s4JV&_;Mi-9Ku_sQsY7QT$Tz$q~6D@D^Hj zYD9V2`4QD4CTLc|K+5=qaT<$VCjZZ1;$gvV$W+%*Rq>43>v982VvC3VK7<3jH2KS0tb@MgwMxdc}z{ zydRd`fpQioN)WL z3cL1Ty{0R^_u1cfawCtclIs#gBwo4q`#h4HFdmUch*x9cQIj-dJjR2<=*{HZz4luBwf5R;?X~tk z=k`LXJICO5`-INPNKNeAzw?mJ!;qTVc|_-o&SQ|8*?AmpywAeE>L85RyN>R2hcEA0 z*S(_mfZj&yg4Pn((L1AaY43?W>wBkm&TL)Ly4H2HR`jgfVR84-JFIG-+CIO%#P#bP z-t$%Oc+_@s=j7H>*RQptbwm5bo_`NNqCLO881**XWp1gT;t!Ry$9d%wo_ueF6=7vQ zD&LlA++iN%@>~7r5-rV(Xk3rvNwU@MSY9G}8ar?M7cO1Sy6L1@ZIp^B^J%_`PG4?!YbZetrP>w<7Dy#xqWYD52~V zVX305bb zO*-kF$G3RrW7Oy8=zo53z68(vbL`^qUEbds-SYe`pvIOuQ!jdA=R zI&#DB4(i~i`NKhdq(|%!H{nkPFFay1v;7?0fIk~|;OF8N{5;%)|GR&O-|WohWjLos zWaicEjqI)LAK5>%PvPr+nSGsoldTUP(Vh;z&4#$lG1EArShGncToEvWDZ=ApKye-H& zd%-obx3l-)W#B!CurORK=Q53OURW;C>t?t<+=&xWM&Fy^qHt^YSy&_KSz$rATF!i% z;r4KUctFmCn~gp5t@7a#)z8Bv;mUArxK85bd6=6YmLHxUng0ak`-KtV#&A!#7iYVS z*!K?;!)4*}a1G+)BXCB@6VQ#q>~Ma#ARm&C&nM*j3yw`IO{zFoe3zC+%d?~J$V#v-!5H{$5~ z=KJRdd-wYb7uTR5<@U+34kU9sA_+!(GnH&%Eu4l8djx0`TgcYObLA>zYq zm6p%JCLM#{m*HRLmf~N4<#+;?i(_r#tKB2P_bGmg>xZ{z4nm&}g*EFhe)&cs6+;`N z8lzm7jCE5P>lQNB?PaW^WvqQN)^W|JF|v^we{wS*PqZdrDE@DiKsf)Z;Mxg{kCyI+A|pDWPJ1j2b_1&7nb3UqY>;WWL1yD@=i1t550oso{?n-fQIC@$U_APWXKkq@blN zCchsbog;dANFjnqy8R`s@per!(&NJ(%-6U*o}_O{KHu-xdRoX~K7BVnkw%maX*~G@ zd4T<5+M@UH%1HB&&}(|Fq}i_0ueJW{C-qRS?9@Q(7hjKO2gt{h?P8B0Poy>9`ZKyL zi+LwK<|)!>TKS-9^ol?1^FxyMuq~w5vYbheFLD@7ct+C9DeDv(;g!@Qo&ej`vVE*Q z^wsGRXJx+fygElr$NVX579|nS#fmHQ8HLyUeFM{xUgpB;+^LeUyppzbTp}ep*ILgW zGVhum4SUHSw!4e)U(>QLv;3Jdr<%W0U|W(FAIH+-0mXD5I1tl&bL{z}kml3!-{5&_ z{urL;z?;ePvvQmcApc>>|Fz^lBKc3^xibGfo-6XF@VpUTO@`CR$0h#>$;X}v`M+a6 zdf&j_U^dPzuLFNz0}2`FD$>$t?7C#GCEB2}r1|vSuo2}NmUYNn=RQE~Rd{GzrQibb zT{S#3rLAH{o}D|OhKKj8c##q3epJIRjdQp@cMsU*1<66iCtSQ=G}cQ+@D;c@3GQqwM~mvPXT~Fr-6K{< zNI2#Eg$Zt<;Y70hB7?Y(b+~P%PBRe)?q!*@&(-oX8+{$*faP z8j(^Tv|?F#vdVncWV;!uC+=+2VpVq`wBlMPPfIP`Tr0C|Hz%M&K%qMiO>6yX;VfNk zmpcZnlhJ#mL<_ODB5h^K4eguK^Q4wE4>MoV%$JC#)T9t3#KuXPuBq*i;6C(`I!>|wlv61m3{3bp=PtM;{S z@$veNSs2|rJG_~vIz-nBB*+chAm`)_a%RJL4q;#9>HKX-L%ZA4dHCr5BEj?N{2hR= z5tvWs;kAoi3_Pz99^o_vX-c%tn2bU+3;U!xN;L*y$ht^I;4V$1gY*rB3Rv|9>7xX* zEw+y=JYu}vd02LtNuDaxYao{AMG955?4E+Nb?Nr9`N7(d8Pak{+kA1=^W?iL*2%Q` zA{EVT-jA#+{&gx{VYsnStg*nfKss*iIa-5^um#nR)qeij`l;ITFQ}7tAw%RYdOv|_ z^M6Rilkd1Lc?PDC`bO2=bKp+8;wwcDNybiZ$^0d18Q%U-k4z!n`QQ&{S$npXLL%AD zO)AG;=mm^Dt~{A@j9nx~+GDzaVx4nMI(P@8SRYIaRB|w`#{i73NW)cRN+hgUm8rZ% zf7P_5=o{x&%7|E}?Z#uTRm;HJ0AHGG3$FupLTf|~5e0vvlh(4IjV%ftDY%LTRBzXL$b|H`lzH+btLV_cbfHldih4BG0f+4_sM6dDLx&16Ysg&It>y2wh@_px=H4r3OS z*E}f}N^6g|XAB1olHla#Of1LK1wG$D##bETh(sl4$|UN@>W;$z5~+*M9wOm zT4$!E6>?l#5sh2gIhFWmfw;;#H}H@LSL~&(p-!z;Ee*!%oV~YD(dUcHQl=9iVcNP# z154os2Q6`>)UdJ?I6)fRcdF5xyaCNA8_>+&fM!ma15Ti3czV_!kw@mV3L0|F@vU3w zo7zR^wnHBEIV~db+t=tP6!hHfIk%6|(-IP%Sfej|MT-^i42Sh|MoC}u9r0H8aaKRADCv){(O($pMNS?5?ymGlG#=>}*6433=!K*Bo2^Pu zStI?eHTu^gz1U&rURQdtz@&epM*n6(uXge+qo-9S{o5(M$ISzqFWu`8FnU^Q(EEWk zdT4N^H@|O~(u?&bJw_o?$bFgGaE`B`klJ)>Pkp>@YmXM5{o5?oZ~Rn`NpE2ie`3U& zte>Xk#kLcFdfHz^(1pKZ(TSfQ%ZpZbZc!|6@FfZt>#c}Anf1H7pcfy`-BWAt!B}2x zg!@&|p3=Xkc=Si%_s!p+HMd>A@kiJ4=S6)A|Er{r8dB-4zAqxZ!8qNl$~PkP$`5Kv z@WW$0Vpm45FWP0ni|!+S*IIdMQPR>L5kD>+pWn^!u-$^+J>qq~yu{Yomce`0>W}N6 z9u4cq`Y-yYy9jNcY2v3^d$i+(r^oiJyzY_JdK1rGGQ9=HAB^8plicz`pZG(0#how1 z6IP3h^741I0gF8%e814E%_sb>!m?Xnds7Urw|5<4Us7MeKaI>3^?UY!?a`*Q{3%9H zJ5Kma!}HcF;kgFWUK3tmFl{v9l?DIIHjA_X0lG(omidGpHO?vS8^xI&`qoypT0u~h zwfMUsP;@{>Og@SDj^~hWj&QWlHA0$7MU*gI~XJWA;7P-D&%jF z-^|BVKpFASD=czG`g?0I&U2#PGKo(5hm%>3PoGE+;@so zxp3!5#1ZnHkM zi>@e7JIfVk`k>C2f^V!t>8M3G=O|-ne{chnWgl(tlcNCQvC)`>5NqDmpfmmRT`aJkjTo-hSUD7HlD^YvXwYK=yTQJCe6xyQVRbC zm(F>N?J+JDeN(2NL7UQlq$zdU0&g`i&B&moP5(>2p2jPUNN}X*E-cb>N3Y|DZ7XI> zElZp)w(Q(9#swXPbI)3jD2>Gbp}-4g{7wa4;hhDJJRPeLddISi;p%>~T=&)vz8jTF zlJbt!m~GiJqkmGGu-QHHy^|=L~O8lw8nxLQDJ?&`)v#cXpF>R>^Nx e(zGt5CpEN~L4-NtS%SlTT0kWh_i6=}oc{qb=iD;@ diff --git a/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_regular.ttf b/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_regular.ttf deleted file mode 100644 index dff66cc50702c75abd025dcf49f62a4dcc2d72de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273900 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≦fXo=WUuIA1w6wc ze~u{Wb9+aAvsl~X_C7Z%y?9g7;~Om*Y2NJVomZXf_|uD${+1m5eV$(Co5AnzN_)iO zk$!cK{^^8XB#Pf|ReI_g>7U8bAN2I%hmHA_(u)Ts{h=KF;e=lO6#e&@)F53$FBT-#1w4#e0(;y$~(rx=ejI`&Up%eY&ep_joO?K6-ez zZ;d?Mke@c%aGy@gbI z$L}*w?_iv6ru7>UdX)z)CFBucK4MkIU7z>Mf*0FI{6)F)w4$V?KO%liwtvCJ4iDcg z_;DVu;}uA3opm|*k{o|O|GA?ff6V`+eY%R!`k5wvrmK&BobZjlK387X$m+d`=PH@I z1;!toyoHn8@fYEq2fJvuUK2j-C3r;2RiWTX`D8x+2T$P{G9~{PKo*R{csXgi_yk`1;L;*n zu4#}j9#7NcqBqxpaA`dSl2aGmRf-4W=y;mTgpbg>zPP&F$jOQ>C&$L>91@1kp|t3N z?%3F$zRbhesr_e;#?JhVV~%+OK(AA~5aQvF4JLDA#UKi02W>~F;z{`Rx8t1_?57GGZHnfb25qGv4sLJr1xj<;J* zqciQp$t?S)+RpMgw@K(#%d_;qaP;19yxs5vskO00GBWE{AL;y5%O`g0SgJmL6Tsx` z@K9HueoMdv!y5FQL1)mnE4|<-U#ym#UG;WW`YSra{^E>6@syzG5%C3u1Wx3UnrAxa z$RF!G(Ca)SKR=%2V$2)T8gFTS=EhQSe^Q+4g*ojYT|MH4inCsg9fIx`{H#%kbBo-R z+L&t;a=P?sl?V5yls5UPU&L5=U7$ROk=Al7vpB+94p9T9WKJlZ zuSe#nXszbEdW1f|PiRlHz^~sij&6Q4rbXG}om=?y^OQMN#611nq^(VBlh?nGJ)cAa zk18M9O=+DmpCtYEwgMzlm+BDf!cNA?MkAMA0v-Ylb}7=wWKS?7)jl1Ol>aizg<7@I zm=zga`JTh64cO+!&i9eE`2NA3s|9!3tkNf?$lr}iddz)nk8vr#H)VPs>Xh~)O|8=! zc&mYFMh0El*?-B`(|Dy34fgc3Zl0bidhI`aTQXwmS+dq { - baseline - } - - FontTheme.CUSTOM -> { - Typography( - displayLarge = baseline.displayLarge.copy(fontFamily = jetbrainsMonoFontFamily), - displayMedium = baseline.displayMedium.copy(fontFamily = jetbrainsMonoFontFamily), - displaySmall = baseline.displaySmall.copy(fontFamily = jetbrainsMonoFontFamily), - headlineLarge = baseline.headlineLarge.copy(fontFamily = jetbrainsMonoFontFamily), - headlineMedium = baseline.headlineMedium.copy(fontFamily = jetbrainsMonoFontFamily), - headlineSmall = baseline.headlineSmall.copy(fontFamily = jetbrainsMonoFontFamily), - titleLarge = baseline.titleLarge.copy(fontFamily = jetbrainsMonoFontFamily), - titleMedium = baseline.titleMedium.copy(fontFamily = jetbrainsMonoFontFamily), - titleSmall = baseline.titleSmall.copy(fontFamily = jetbrainsMonoFontFamily), - bodyLarge = baseline.bodyLarge.copy(fontFamily = interFontFamily), - bodyMedium = baseline.bodyMedium.copy(fontFamily = interFontFamily), - bodySmall = baseline.bodySmall.copy(fontFamily = interFontFamily), - labelLarge = baseline.labelLarge.copy(fontFamily = interFontFamily), - labelMedium = baseline.labelMedium.copy(fontFamily = interFontFamily), - labelSmall = baseline.labelSmall.copy(fontFamily = interFontFamily), - ) - } - } +fun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography { + if (fontTheme == FontTheme.SYSTEM) return baseline + + val serif = fraunces + val sans = interTight + + fun TextStyle.fraunces(weight: FontWeight) = copy( + fontFamily = serif, + fontWeight = weight, + fontStyle = FontStyle.Italic, + letterSpacing = (-0.02).em, + ) + + fun TextStyle.sans(weight: FontWeight) = copy( + fontFamily = sans, + fontWeight = weight, + textDecoration = TextDecoration.None, + ) + + return Typography( + // Editorial voice — Fraunces italic + displayLarge = baseline.displayLarge.fraunces(FontWeight.SemiBold).copy( + letterSpacing = (-0.025).em, + fontSize = 36.sp, + ), + displayMedium = baseline.displayMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 32.sp), + displaySmall = baseline.displaySmall.fraunces(FontWeight.SemiBold).copy(fontSize = 28.sp), + headlineLarge = baseline.headlineLarge.fraunces(FontWeight.SemiBold).copy(fontSize = 26.sp), + headlineMedium = baseline.headlineMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 22.sp), + headlineSmall = baseline.headlineSmall.fraunces(FontWeight.SemiBold).copy(fontSize = 20.sp), + titleLarge = baseline.titleLarge.fraunces(FontWeight.SemiBold).copy(fontSize = 18.sp), + titleMedium = baseline.titleMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 16.sp), + titleSmall = baseline.titleSmall.fraunces(FontWeight.SemiBold).copy(fontSize = 14.sp), + // Body voice — Inter Tight + bodyLarge = baseline.bodyLarge.sans(FontWeight.Normal).copy(fontSize = 14.sp), + bodyMedium = baseline.bodyMedium.sans(FontWeight.Normal).copy(fontSize = 13.sp), + bodySmall = baseline.bodySmall.sans(FontWeight.Medium).copy(fontSize = 12.sp), + labelLarge = baseline.labelLarge.sans(FontWeight.SemiBold), + labelMedium = baseline.labelMedium.sans(FontWeight.SemiBold), + labelSmall = baseline.labelSmall.sans(FontWeight.SemiBold), + ) +} From 5943de2909adcee77c3dffd6efd7d72c5ff67a92 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:13:52 +0500 Subject: [PATCH 006/172] feat(theme): add composition locals for token surfaces --- .../rainxch/core/presentation/theme/Locals.kt | 113 ++++++++++++++++++ .../rainxch/core/presentation/theme/Theme.kt | 36 ++++-- 2 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt new file mode 100644 index 000000000..cdf58f196 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt @@ -0,0 +1,113 @@ +package zed.rainxch.core.presentation.theme + +import androidx.compose.runtime.staticCompositionLocalOf +import zed.rainxch.core.presentation.theme.tokens.Tokens + +/** + * Composition locals exposing the design tokens that Material 3 doesn't natively cover. + * Provided by `GithubStoreTheme` / `GhsTheme`. Reading any of these outside a theme + * scope falls back to a sensible default rather than throwing. + */ +val LocalPalette = staticCompositionLocalOf { Tokens.Nord.light } + +val LocalStatusColors = staticCompositionLocalOf { defaultStatusColors } + +val LocalThresholds = staticCompositionLocalOf { defaultThresholds } + +val LocalMotion = staticCompositionLocalOf { defaultMotion } + +val LocalSpacing = staticCompositionLocalOf { defaultSpacing } + +data class StatusColors( + val freshnessHot: androidx.compose.ui.graphics.Color, + val freshnessFresh: androidx.compose.ui.graphics.Color, + val freshnessWarm: androidx.compose.ui.graphics.Color, + val freshnessCool: androidx.compose.ui.graphics.Color, + val freshnessDormant: androidx.compose.ui.graphics.Color, + val waxIntact: androidx.compose.ui.graphics.Color, + val waxCracked: androidx.compose.ui.graphics.Color, + val waxOpen: androidx.compose.ui.graphics.Color, + val permLow: androidx.compose.ui.graphics.Color, + val permModerate: androidx.compose.ui.graphics.Color, + val permHigh: androidx.compose.ui.graphics.Color, + val trendRising: androidx.compose.ui.graphics.Color, + val trendFlat: androidx.compose.ui.graphics.Color, + val trendFalling: androidx.compose.ui.graphics.Color, +) + +data class ThresholdSet( + val freshness: List, + val stars: List, + val maintenance: List, +) + +data class MotionTokens( + val tapHighlightMs: Int, + val paletteCrossfadeMs: Int, + val sheetSlideMs: Int, + val scrimFadeMs: Int, + val toastSlideMs: Int, + val toastFadeMs: Int, + val heartbeatScaleFrom: Float, + val heartbeatScaleTo: Float, + val heartbeatHaloFromScale: Float, + val heartbeatHaloToScale: Float, + val heartbeatHaloFromAlpha: Float, + val heartbeatHaloToAlpha: Float, +) + +data class SpacingTokens( + val xs: Int, + val sm: Int, + val md: Int, + val lg: Int, + val xl: Int, + val xxl: Int, +) + +internal val defaultStatusColors = StatusColors( + freshnessHot = Tokens.Status.Freshness.hot, + freshnessFresh = Tokens.Status.Freshness.fresh, + freshnessWarm = Tokens.Status.Freshness.warm, + freshnessCool = Tokens.Status.Freshness.cool, + freshnessDormant = Tokens.Status.Freshness.dormant, + waxIntact = Tokens.Status.Wax.intact, + waxCracked = Tokens.Status.Wax.cracked, + waxOpen = Tokens.Status.Wax.open, + permLow = Tokens.Status.Perm.low, + permModerate = Tokens.Status.Perm.moderate, + permHigh = Tokens.Status.Perm.high, + trendRising = Tokens.Status.Trend.rising, + trendFlat = Tokens.Status.Trend.flat, + trendFalling = Tokens.Status.Trend.falling, +) + +internal val defaultThresholds = ThresholdSet( + freshness = Tokens.Thresholds.freshness, + stars = Tokens.Thresholds.stars, + maintenance = Tokens.Thresholds.maintenance, +) + +internal val defaultMotion = MotionTokens( + tapHighlightMs = Tokens.Motion.tapHighlightMs, + paletteCrossfadeMs = Tokens.Motion.paletteCrossfadeMs, + sheetSlideMs = Tokens.Motion.sheetSlideMs, + scrimFadeMs = Tokens.Motion.scrimFadeMs, + toastSlideMs = Tokens.Motion.toastSlideMs, + toastFadeMs = Tokens.Motion.toastFadeMs, + heartbeatScaleFrom = Tokens.Motion.heartbeatScaleFrom, + heartbeatScaleTo = Tokens.Motion.heartbeatScaleTo, + heartbeatHaloFromScale = Tokens.Motion.heartbeatHaloFromScale, + heartbeatHaloToScale = Tokens.Motion.heartbeatHaloToScale, + heartbeatHaloFromAlpha = Tokens.Motion.heartbeatHaloFromAlpha, + heartbeatHaloToAlpha = Tokens.Motion.heartbeatHaloToAlpha, +) + +internal val defaultSpacing = SpacingTokens( + xs = Tokens.Spacing.xs, + sm = Tokens.Spacing.sm, + md = Tokens.Spacing.md, + lg = Tokens.Spacing.lg, + xl = Tokens.Spacing.xl, + xxl = Tokens.Spacing.xxl, +) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index ccbbeb8f9..1b7426b62 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.presentation.theme.tokens.Tokens @@ -14,11 +15,11 @@ import zed.rainxch.core.presentation.utils.toTokenPalette /** * App-wide theme entry point. Resolves the active [AppTheme] palette + light/dark/amoled * mode to a Material 3 [ColorScheme] backed by the design tokens in - * [zed.rainxch.core.presentation.theme.tokens.Tokens]. + * [zed.rainxch.core.presentation.theme.tokens.Tokens], plus provides composition locals + * that expose richer surfaces (status colors, thresholds, motion, spacing). * - * Composition locals exposing the richer token surface (status colors, thresholds, motion, - * spacing) are added in the upcoming `GhsTheme` wrapper — kept here as a thin shim for the - * existing `Main.kt` call site until P6 chrome polish swaps in the new composable. + * `Main.kt` is the only call site — kept under the legacy `GithubStoreTheme` name until + * P6 chrome polish swaps in the user-facing rename to `GhsTheme`. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -34,12 +35,23 @@ fun GithubStoreTheme( isAmoledTheme -> Tokens.Mode.AMOLED else -> Tokens.Mode.DARK } - val scheme = colorSchemeFor(palette = appTheme.toTokenPalette(), mode = mode) - MaterialExpressiveTheme( - colorScheme = scheme, - typography = getAppTypography(fontTheme), - motionScheme = MotionScheme.expressive(), - shapes = MaterialTheme.shapes, - content = content, - ) + val tokenPalette = appTheme.toTokenPalette() + val palette = Tokens.palette(tokenPalette, mode) + val scheme = colorSchemeFor(palette = tokenPalette, mode = mode) + + CompositionLocalProvider( + LocalPalette provides palette, + LocalStatusColors provides defaultStatusColors, + LocalThresholds provides defaultThresholds, + LocalMotion provides defaultMotion, + LocalSpacing provides defaultSpacing, + ) { + MaterialExpressiveTheme( + colorScheme = scheme, + typography = getAppTypography(fontTheme), + motionScheme = MotionScheme.expressive(), + shapes = MaterialTheme.shapes, + content = content, + ) + } } From a026dbec660a21f297183950e0bf794402d9595a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:15:01 +0500 Subject: [PATCH 007/172] feat(theme): add ThemeMode unified two-axis persistence --- .../data/repository/TweaksRepositoryImpl.kt | 21 +++++++++++++++++++ .../domain/repository/TweaksRepository.kt | 11 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index bb94bbde7..9e4114271 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -28,8 +28,10 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerType +import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.domain.model.TranslationProvider import zed.rainxch.core.domain.repository.TweaksRepository +import kotlinx.coroutines.flow.combine import zed.rainxch.core.data.secure.safeDelete import zed.rainxch.core.data.secure.safeGet import zed.rainxch.core.data.secure.safeGetFlow @@ -82,6 +84,25 @@ class TweaksRepositoryImpl( override fun getAmoledTheme(): Flow = gatedGetFlow(K_AMOLED, false) override suspend fun setAmoledTheme(enabled: Boolean) { migrationDeferred.await(); ksafe.safePut(K_AMOLED, enabled) } + override fun getThemeMode(): Flow = + combine(getIsDarkTheme(), getAmoledTheme()) { isDark, amoled -> + when { + isDark == null -> ThemeMode.SYSTEM + !isDark -> ThemeMode.LIGHT + amoled -> ThemeMode.AMOLED + else -> ThemeMode.DARK + } + } + + override suspend fun setThemeMode(mode: ThemeMode) { + when (mode) { + ThemeMode.SYSTEM -> setDarkTheme(null) + ThemeMode.LIGHT -> { setDarkTheme(false); setAmoledTheme(false) } + ThemeMode.DARK -> { setDarkTheme(true); setAmoledTheme(false) } + ThemeMode.AMOLED -> { setDarkTheme(true); setAmoledTheme(true) } + } + } + override fun getFontTheme(): Flow = gatedGetFlow(K_FONT, "").map { FontTheme.fromName(it.ifEmpty { null }) } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 65005bfdf..6dcbcdcff 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -7,6 +7,7 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerType +import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.domain.model.TranslationProvider interface TweaksRepository { @@ -22,6 +23,16 @@ interface TweaksRepository { suspend fun setAmoledTheme(enabled: Boolean) + /** + * Unified two-axis theme mode (LIGHT / DARK / AMOLED / SYSTEM) derived from + * the underlying [getIsDarkTheme] (null = SYSTEM, false = LIGHT, true = DARK) + * and [getAmoledTheme] (true only when DARK, lifts to AMOLED). Setter splits + * the value back into the two boolean keys — no schema migration required. + */ + fun getThemeMode(): Flow + + suspend fun setThemeMode(mode: ThemeMode) + fun getFontTheme(): Flow suspend fun setFontTheme(fontTheme: FontTheme) From f2660a42ee0db0a48887412272d2b78602b4d274 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:19:55 +0500 Subject: [PATCH 008/172] feat(vocabulary): add silent + expressive primitives --- .../presentation/vocabulary/CookieShape.kt | 42 ++++ .../presentation/vocabulary/DownloadWeight.kt | 43 ++++ .../core/presentation/vocabulary/Freshness.kt | 23 ++ .../presentation/vocabulary/FreshnessRing.kt | 87 +++++++ .../core/presentation/vocabulary/Heartbeat.kt | 125 ++++++++++ .../presentation/vocabulary/LicensePosture.kt | 56 +++++ .../core/presentation/vocabulary/PermDot.kt | 46 ++++ .../presentation/vocabulary/PlatformGlyph.kt | 143 ++++++++++++ .../presentation/vocabulary/SignalBars.kt | 44 ++++ .../core/presentation/vocabulary/Squiggle.kt | 50 ++++ .../core/presentation/vocabulary/StarTier.kt | 52 +++++ .../presentation/vocabulary/TopicGlyph.kt | 218 ++++++++++++++++++ .../presentation/vocabulary/VersionDelta.kt | 66 ++++++ .../presentation/vocabulary/VersionStack.kt | 43 ++++ .../core/presentation/vocabulary/WaxSeal.kt | 104 +++++++++ 15 files changed, 1142 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt new file mode 100644 index 000000000..4d9142546 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt @@ -0,0 +1,42 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +/** + * 9-petal Material 3 Expressive flower silhouette. Used at exactly 3 touchpoints + * (DESIGN.md §4.3 + §5.3): brand "G" mark, user avatar tile, active bottom-nav tab. + * + * Path translated from `tokens.json.shape.cookie.path` (viewBox 100×100), rescaled + * to fill the [Size] passed by Compose. Used via `Modifier.clip(CookieShape)` or as + * a `Shape` parameter on `Surface`/`Box`. + */ +object CookieShape : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + val sx = size.width / 100f + val sy = size.height / 100f + val path = Path().apply { + moveTo(50f * sx, 4f * sy) + cubicTo(62f * sx, 4f * sy, 66f * sx, 12f * sy, 76f * sx, 12f * sy) + cubicTo(86f * sx, 12f * sy, 91f * sx, 22f * sy, 91f * sx, 32f * sy) + cubicTo(95f * sx, 40f * sy, 100f * sx, 50f * sy, 94f * sx, 58f * sy) + cubicTo(96f * sx, 70f * sy, 90f * sx, 82f * sy, 80f * sx, 86f * sy) + cubicTo(72f * sx, 90f * sy, 64f * sx, 96f * sy, 54f * sx, 96f * sy) + cubicTo(44f * sx, 96f * sy, 36f * sx, 95f * sy, 26f * sx, 92f * sy) + cubicTo(16f * sx, 90f * sy, 10f * sx, 80f * sy, 8f * sx, 70f * sy) + cubicTo(4f * sx, 62f * sy, 0f, 54f * sy, 6f * sx, 46f * sy) + cubicTo(6f * sx, 34f * sy, 12f * sx, 22f * sy, 22f * sx, 18f * sy) + cubicTo(32f * sx, 12f * sy, 38f * sx, 4f * sy, 50f * sx, 4f * sy) + close() + } + return Outline.Generic(path) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt new file mode 100644 index 000000000..5809593f0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt @@ -0,0 +1,43 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import kotlin.math.log10 +import kotlin.math.min + +/** + * Log-scale dot inside a fixed-size ring. Radius = `log10(downloads)`. Replaces + * "62.8k downloads" prose with adoption magnitude (DESIGN.md §4.1). + */ +@Composable +fun DownloadWeight( + downloads: Long, + modifier: Modifier = Modifier, + sizeDp: Int = 16, +) { + val ringColor = MaterialTheme.colorScheme.outline + val dotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f) + val log = log10(downloads.coerceAtLeast(1L).toDouble()).toFloat() + Canvas(modifier = modifier.size(sizeDp.dp)) { + val cx = size.width / 2f + val cy = size.height / 2f + val ringRadius = size.width / 2f - 1.dp.toPx() + val dotRadius = min(size.width / 2f, 2.dp.toPx() + log * 1.6.dp.toPx()) + drawCircle( + color = ringColor, + radius = ringRadius, + center = androidx.compose.ui.geometry.Offset(cx, cy), + style = Stroke(width = 1.dp.toPx()), + ) + drawCircle( + color = dotColor, + radius = dotRadius, + center = androidx.compose.ui.geometry.Offset(cx, cy), + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt new file mode 100644 index 000000000..8caa6102e --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt @@ -0,0 +1,23 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.ui.graphics.Color +import zed.rainxch.core.presentation.theme.tokens.Tokens + +/** Maintenance / freshness buckets (DESIGN.md §2.3, thresholds in Tokens). */ +enum class FreshnessState { HOT, FRESH, WARM, COOL, DORMANT } + +data class Freshness(val state: FreshnessState, val color: Color, val ringFraction: Float) + +fun freshnessOf(daysSinceRelease: Int): Freshness { + val b = Tokens.Thresholds.freshness.first { + it.maxDaysInclusive == null || daysSinceRelease <= it.maxDaysInclusive + } + val state = when (b.state) { + "hot" -> FreshnessState.HOT + "fresh" -> FreshnessState.FRESH + "warm" -> FreshnessState.WARM + "cool" -> FreshnessState.COOL + else -> FreshnessState.DORMANT + } + return Freshness(state, b.color, b.ringFraction) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt new file mode 100644 index 000000000..29ff2b13c --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt @@ -0,0 +1,87 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +/** + * Squircle icon tile wrapped in a freshness ring that drains over time. Replaces + * "Released N days ago" prose (DESIGN.md §4.1). Ring fraction + color encoded by + * the bucket from [freshnessOf]. Drop-in for app avatars on Home, Library, and + * Detail hero. + */ +@Composable +fun FreshnessRing( + daysSinceRelease: Int, + modifier: Modifier = Modifier, + sizeDp: Int = 64, + strokeDp: Float = if (sizeDp >= 60) 2.5f else 2f, + content: @Composable () -> Unit, +) { + val f = freshnessOf(daysSinceRelease) + val ringSize = sizeDp + 14 + Box( + modifier = modifier.size(ringSize.dp), + contentAlignment = Alignment.Center, + ) { + FreshnessArc( + color = f.color, + fraction = f.ringFraction, + strokeDp = strokeDp, + modifier = Modifier.size(ringSize.dp), + ) + Box( + modifier = Modifier.size(sizeDp.dp), + contentAlignment = Alignment.Center, + ) { + content() + } + } +} + +@Composable +private fun FreshnessArc( + color: Color, + fraction: Float, + strokeDp: Float, + modifier: Modifier, +) { + Canvas(modifier = modifier) { + val sw = strokeDp.dp.toPx() + val radius = (size.minDimension - sw - 2.dp.toPx()) / 2f + val topLeft = Offset( + (size.width - radius * 2) / 2f, + (size.height - radius * 2) / 2f, + ) + val arcSize = Size(radius * 2, radius * 2) + // Background track, low opacity + drawArc( + color = color.copy(alpha = 0.14f), + startAngle = 0f, + sweepAngle = 360f, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = sw, cap = StrokeCap.Round), + ) + // Foreground drain + drawArc( + color = color, + startAngle = -90f, + sweepAngle = 360f * fraction, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(width = sw, cap = StrokeCap.Round), + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt new file mode 100644 index 000000000..fa2ae0ed6 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt @@ -0,0 +1,125 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalMotion +import zed.rainxch.core.presentation.theme.LocalStatusColors + +/** + * Pulsing dot that animates per maintenance state. Periods (DESIGN.md §6.1): + * active 1.4s / recent 2.4s / quiet 4.2s / dormant = static grey (no animation). + * + * Per the android-compose-ui skill, animation drives a `graphicsLayer` to avoid + * recomposition; the surrounding [Box] hosts the layout. Don't pair with + * [FreshnessRing] in dense list rows — they compete for the same "is this alive" + * attention (DESIGN.md §6.1 closing rule). + */ +@Composable +fun Heartbeat( + daysSinceCommit: Int, + modifier: Modifier = Modifier, + sizeDp: Int = 8, +) { + val status = LocalStatusColors.current + val motion = LocalMotion.current + val periodMs = heartbeatPeriodMs(daysSinceCommit) + val color = heartbeatColor(daysSinceCommit, status) + + if (periodMs == null) { + // Dormant — static muted dot + Box( + modifier = modifier + .size(sizeDp.dp) + .background(color.copy(alpha = 0.4f), CircleShape), + ) + return + } + + val transition = rememberInfiniteTransition(label = "heartbeat") + val scale by transition.animateFloat( + initialValue = motion.heartbeatScaleFrom, + targetValue = motion.heartbeatScaleTo, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = periodMs, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "heartbeat-scale", + ) + val haloScale by transition.animateFloat( + initialValue = motion.heartbeatHaloFromScale, + targetValue = motion.heartbeatHaloToScale, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = periodMs, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = "heartbeat-halo-scale", + ) + val haloAlpha by transition.animateFloat( + initialValue = motion.heartbeatHaloFromAlpha, + targetValue = motion.heartbeatHaloToAlpha, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = periodMs, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + label = "heartbeat-halo-alpha", + ) + + Box( + modifier = modifier.size(sizeDp.dp), + contentAlignment = Alignment.Center, + ) { + // Halo via drawBehind to avoid recomposition + Box( + modifier = Modifier + .size(sizeDp.dp) + .drawBehind { + val r = (size.minDimension / 2f) * haloScale + drawCircle( + color = color.copy(alpha = haloAlpha), + radius = r, + center = Offset(size.width / 2f, size.height / 2f), + ) + }, + ) + // Dot scales via graphicsLayer (no recomposition) + Box( + modifier = Modifier + .size(sizeDp.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .background(color, CircleShape), + ) + } +} + +private fun heartbeatPeriodMs(days: Int): Int? = when { + days <= 1 -> 1400 + days <= 7 -> 2400 + days <= 30 -> 4200 + else -> null +} + +private fun heartbeatColor(days: Int, status: zed.rainxch.core.presentation.theme.StatusColors): Color = when { + days <= 7 -> status.freshnessFresh + days <= 30 -> status.freshnessWarm + else -> status.freshnessDormant +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt new file mode 100644 index 000000000..e65221179 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt @@ -0,0 +1,56 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.jetbrainsMono +import zed.rainxch.core.presentation.theme.tokens.Tokens + +/** + * Filled © tile (copyleft) or dashed · tile (permissive). Replaces SPDX text label + * (DESIGN.md §4.1). Uses [Tokens.Licenses] SPDX → posture map. + */ +@Composable +fun LicensePosture( + spdx: String?, + modifier: Modifier = Modifier, + sizeDp: Int = 14, +) { + val heavy = spdx != null && spdx in Tokens.Licenses.copyleft + val ink = MaterialTheme.colorScheme.onSurface + val bg = MaterialTheme.colorScheme.background + val mono = jetbrainsMono + Box( + modifier = modifier + .size(sizeDp.dp) + .background( + color = if (heavy) ink else Color.Transparent, + shape = RoundedCornerShape(3.dp), + ) + .border( + width = 1.4.dp, + color = ink, + shape = RoundedCornerShape(3.dp), + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = if (heavy) "©" else "·", + color = if (heavy) bg else ink, + fontFamily = mono, + fontWeight = FontWeight.Bold, + fontSize = (sizeDp * 0.55f).sp, + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt new file mode 100644 index 000000000..4dfb2c88e --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt @@ -0,0 +1,46 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalStatusColors + +/** Permission risk classification mapped to a single colored dot. */ +enum class PermLevel { LOW, MODERATE, HIGH } + +/** + * Single-dot heat indicator for permission risk. Replaces "App permissions" wall-of-text + * (DESIGN.md §4.1). Optional 3px halo ring for emphasis on hero surfaces. + */ +@Composable +fun PermDot( + level: PermLevel, + modifier: Modifier = Modifier, + size: Int = 8, + ring: Boolean = false, +) { + val status = LocalStatusColors.current + val color = when (level) { + PermLevel.LOW -> status.permLow + PermLevel.MODERATE -> status.permModerate + PermLevel.HIGH -> status.permHigh + } + val ringExtraDp = if (ring) 6 else 0 + Canvas(modifier = modifier.size((size + ringExtraDp).dp)) { + val cx = this.size.width / 2f + val cy = this.size.height / 2f + val dotRadius = size.dp.toPx() / 2f + if (ring) { + drawCircle( + color = color.copy(alpha = 0.2f), + radius = dotRadius + 3.dp.toPx(), + center = Offset(cx, cy), + ) + } + drawCircle(color = color, radius = dotRadius, center = Offset(cx, cy)) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt new file mode 100644 index 000000000..7808e6be0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt @@ -0,0 +1,143 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +/** Supported platforms in the silent vocabulary. */ +enum class PlatformKind { ANDROID, WINDOWS, MACOS, LINUX } + +/** + * Filled silhouette when supported, dashed outline at 32% alpha when not. Replaces + * "Android · Windows · Linux" prose (DESIGN.md §4.1). Always monochrome — never + * carries the per-app accent. + */ +@Composable +fun PlatformGlyph( + kind: PlatformKind, + supported: Boolean, + modifier: Modifier = Modifier, + sizeDp: Int = 18, +) { + val ink = MaterialTheme.colorScheme.onSurface + val ink2 = MaterialTheme.colorScheme.onSurfaceVariant + val bg = MaterialTheme.colorScheme.background + val color = if (supported) ink else ink2 + val alpha = if (supported) 1f else 0.32f + Canvas(modifier = modifier.size(sizeDp.dp)) { + when (kind) { + PlatformKind.ANDROID -> drawAndroid(color, bg, supported, alpha) + PlatformKind.WINDOWS -> drawWindows(color, supported, alpha) + PlatformKind.MACOS -> drawMacos(color, supported, alpha) + PlatformKind.LINUX -> drawLinux(color, supported, alpha) + } + } +} + +private fun DrawScope.drawAndroid(c: Color, bg: Color, on: Boolean, alpha: Float) { + val s = size.minDimension + val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) + val rect = androidx.compose.ui.geometry.Rect( + offset = Offset(s * (6f / 24f), s * (3.5f / 24f)), + size = Size(s * (12f / 24f), s * (17f / 24f)), + ) + val cr = CornerRadius(s * (2.6f / 24f), s * (2.6f / 24f)) + if (on) { + drawRoundRect(color = c.copy(alpha = alpha), topLeft = rect.topLeft, size = rect.size, cornerRadius = cr) + } else { + drawRoundRect( + color = c.copy(alpha = alpha), + topLeft = rect.topLeft, + size = rect.size, + cornerRadius = cr, + style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash), + ) + } + drawCircle( + color = if (on) bg else Color.Transparent, + radius = s * (0.9f / 24f), + center = Offset(s * (12f / 24f), s * (17.5f / 24f)), + ) + if (!on) { + drawCircle( + color = c.copy(alpha = alpha), + radius = s * (0.9f / 24f), + center = Offset(s * (12f / 24f), s * (17.5f / 24f)), + style = Stroke(width = 1f.dp.toPx()), + ) + } +} + +private fun DrawScope.drawWindows(c: Color, on: Boolean, alpha: Float) { + val s = size.minDimension + val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) + val cellSize = Size(s * (6f / 24f), s * (6f / 24f)) + val cr = CornerRadius(s * (0.5f / 24f), s * (0.5f / 24f)) + listOf(5f to 5f, 13f to 5f, 5f to 13f, 13f to 13f).forEach { (x, y) -> + val tl = Offset(s * (x / 24f), s * (y / 24f)) + if (on) { + drawRoundRect(color = c.copy(alpha = alpha), topLeft = tl, size = cellSize, cornerRadius = cr) + } else { + drawRoundRect( + color = c.copy(alpha = alpha), + topLeft = tl, + size = cellSize, + cornerRadius = cr, + style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash), + ) + } + } +} + +private fun DrawScope.drawMacos(c: Color, on: Boolean, alpha: Float) { + val s = size.minDimension + val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) + val center = Offset(s * (12f / 24f), s * (13f / 24f)) + val radius = s * (7f / 24f) + if (on) { + drawCircle(color = c.copy(alpha = alpha), radius = radius, center = center) + } else { + drawCircle( + color = c.copy(alpha = alpha), + radius = radius, + center = center, + style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash), + ) + } + val stem = Path().apply { + moveTo(s * (12f / 24f), s * (6f / 24f)) + quadraticTo(s * (13.5f / 24f), s * (4f / 24f), s * (15f / 24f), s * (4f / 24f)) + } + drawPath(stem, c.copy(alpha = alpha), style = Stroke(width = (if (on) 1.5f else 1.4f).dp.toPx(), pathEffect = dash)) +} + +private fun DrawScope.drawLinux(c: Color, on: Boolean, alpha: Float) { + val s = size.minDimension + val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) + val hex = Path().apply { + moveTo(s * (12f / 24f), s * (3.5f / 24f)) + lineTo(s * (19.5f / 24f), s * (7.5f / 24f)) + lineTo(s * (19.5f / 24f), s * (16.5f / 24f)) + lineTo(s * (12f / 24f), s * (20.5f / 24f)) + lineTo(s * (4.5f / 24f), s * (16.5f / 24f)) + lineTo(s * (4.5f / 24f), s * (7.5f / 24f)) + close() + } + if (on) { + drawPath(hex, c.copy(alpha = alpha)) + } else { + drawPath(hex, c.copy(alpha = alpha), style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash, join = StrokeJoin.Round)) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt new file mode 100644 index 000000000..1a589d8e3 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt @@ -0,0 +1,44 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalStatusColors + +/** + * 4 ascending bars — WiFi-style mirror / connection strength. Replaces "62 ms latency" + * prose (DESIGN.md §4.1). `level` in 0..4; bars above `level` use outline color. + */ +@Composable +fun SignalBars( + level: Int, + modifier: Modifier = Modifier, + sizeDp: Int = 14, +) { + val activeColor = LocalStatusColors.current.freshnessFresh + val inactiveColor = MaterialTheme.colorScheme.outline + val clampedLevel = level.coerceIn(0, 4) + Canvas(modifier = modifier.size(width = (sizeDp + 4).dp, height = sizeDp.dp)) { + val barW = 2.5.dp.toPx() + val gap = 1.5.dp.toPx() + val cornerR = 1.dp.toPx() + for (i in 0 until 4) { + val barH = size.height * (0.3f + (i + 1) * 0.18f) + val left = i * (barW + gap) + val top = size.height - barH + val c = if (i < clampedLevel) activeColor else inactiveColor + drawRoundRect( + color = c, + topLeft = Offset(left, top), + size = Size(barW, barH), + cornerRadius = CornerRadius(cornerR, cornerR), + ) + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt new file mode 100644 index 000000000..2078c36bc --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt @@ -0,0 +1,50 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +/** + * Hand-drawn wavy underline. Default ~40×5 dp, 1.6dp stroke, primary color at 60%. + * One per section heading (DESIGN.md §4.3). Path translated from + * `tokens.json.shape.squiggle.path` (viewBox 40×5). + */ +@Composable +fun Squiggle( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), +) { + Canvas(modifier = modifier.size(width = 40.dp, height = 5.dp)) { + val sx = size.width / 40f + val sy = size.height / 5f + val path = Path().apply { + moveTo(1f * sx, 3f * sy) + // Initial quadratic + four smooth quadratics with reflected control points + // (T command in SVG): C1=(5,0.5), then reflect through each end-point. + quadraticTo(5f * sx, 0.5f * sy, 9f * sx, 3f * sy) + smoothQuadTo(17f * sx, 3f * sy, prevControl = Offset(5f * sx, 0.5f * sy), prevEnd = Offset(9f * sx, 3f * sy)) + smoothQuadTo(25f * sx, 3f * sy, prevControl = Offset(13f * sx, 5.5f * sy), prevEnd = Offset(17f * sx, 3f * sy)) + smoothQuadTo(33f * sx, 3f * sy, prevControl = Offset(21f * sx, 0.5f * sy), prevEnd = Offset(25f * sx, 3f * sy)) + smoothQuadTo(39f * sx, 3f * sy, prevControl = Offset(29f * sx, 5.5f * sy), prevEnd = Offset(33f * sx, 3f * sy)) + } + drawPath( + path = path, + color = color, + style = Stroke(width = 1.6f.dp.toPx(), cap = StrokeCap.Round), + ) + } +} + +private fun Path.smoothQuadTo(toX: Float, toY: Float, prevControl: Offset, prevEnd: Offset) { + val reflectedX = 2f * prevEnd.x - prevControl.x + val reflectedY = 2f * prevEnd.y - prevControl.y + quadraticTo(reflectedX, reflectedY, toX, toY) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt new file mode 100644 index 000000000..dfacc6220 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt @@ -0,0 +1,52 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * Michelin-style 1-5 star tier from `stargazersCount`. Log-scale buckets + * (DESIGN.md §4.1, thresholds in `Tokens.Thresholds.stars`): + * 1 ★ < 1k, 2 ★ ≥1k, 3 ★ ≥10k, 4 ★ ≥50k, 5 ★ ≥100k. Replaces "62.8k stars". + */ +@Composable +fun StarTier( + stars: Int, + modifier: Modifier = Modifier, + size: Int = 11, + activeColor: Color = Color(0xFFC49652), + inactiveColor: Color = MaterialTheme.colorScheme.outline, +) { + val tier = starTierOf(stars) + val style = TextStyle(fontSize = size.sp) + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(1.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + for (i in 1..5) { + Text( + text = "★", + color = if (i <= tier) activeColor else inactiveColor, + style = style, + ) + } + } +} + +fun starTierOf(stars: Int): Int = when { + stars >= 100_000 -> 5 + stars >= 50_000 -> 4 + stars >= 10_000 -> 3 + stars >= 1_000 -> 2 + else -> 1 +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt new file mode 100644 index 000000000..f4233b8f0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt @@ -0,0 +1,218 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Tokens + +/** + * Micro-pictogram per supported topic (DESIGN.md §4.2). Monochrome — never carries + * an accent. Returns `null` (renders nothing) when the topic isn't in the supported + * set or its alias map. + */ +@Composable +fun TopicGlyph( + topic: String, + modifier: Modifier = Modifier, + sizeDp: Int = 14, + color: Color = LocalContentColor.current, +) { + val resolved = resolveTopic(topic) ?: return + Canvas(modifier = modifier.size(sizeDp.dp)) { + val sw = 1.7f.dp.toPx() + val stroke = Stroke(width = sw, cap = StrokeCap.Round, join = StrokeJoin.Round) + when (resolved) { + "self-hosted" -> drawSelfHosted(color, stroke) + "mobile" -> drawMobile(color, stroke) + "photo" -> drawPhoto(color, stroke) + "video" -> drawVideo(color, stroke) + "book" -> drawBook(color, stroke) + "manga" -> drawManga(color, stroke) + "key" -> drawKey(color, stroke) + "audio" -> drawAudio(color) + "backup" -> drawBackup(color, stroke) + "reader" -> drawReader(color, stroke) + "cross-platform" -> drawCrossPlatform(color, stroke) + "cloud" -> drawCloud(color, stroke) + } + } +} + +private fun resolveTopic(topic: String): String? { + val key = topic.lowercase() + if (key in Tokens.Topics.supported) return key + return Tokens.Topics.aliases[key] +} + +private fun DrawScope.scaled(viewBoxValue: Float) = viewBoxValue / 24f * size.minDimension + +private fun DrawScope.drawSelfHosted(c: Color, s: Stroke) { + val p = Path().apply { + moveTo(scaled(4f), scaled(12f)) + lineTo(scaled(12f), scaled(5f)) + lineTo(scaled(20f), scaled(12f)) + lineTo(scaled(20f), scaled(19f)) + lineTo(scaled(4f), scaled(19f)) + close() + } + drawPath(p, c, style = s) +} + +private fun DrawScope.drawMobile(c: Color, s: Stroke) { + drawRoundRect( + color = c, + topLeft = Offset(scaled(7f), scaled(3f)), + size = Size(scaled(10f), scaled(18f)), + cornerRadius = CornerRadius(scaled(2f), scaled(2f)), + style = s, + ) + drawLine( + color = c, + start = Offset(scaled(11f), scaled(18f)), + end = Offset(scaled(13f), scaled(18f)), + strokeWidth = s.width, + cap = StrokeCap.Round, + ) +} + +private fun DrawScope.drawPhoto(c: Color, s: Stroke) { + drawRoundRect( + color = c, + topLeft = Offset(scaled(3f), scaled(6f)), + size = Size(scaled(18f), scaled(14f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), + style = s, + ) + drawCircle(color = c, radius = scaled(2f), center = Offset(scaled(9f), scaled(13f))) + val mountain = Path().apply { + moveTo(scaled(14f), scaled(16f)) + lineTo(scaled(17f), scaled(12f)) + lineTo(scaled(21f), scaled(17f)) + } + drawPath(mountain, c, style = s) +} + +private fun DrawScope.drawVideo(c: Color, s: Stroke) { + drawRoundRect( + color = c, + topLeft = Offset(scaled(3f), scaled(6f)), + size = Size(scaled(14f), scaled(12f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), + style = s, + ) + val triangle = Path().apply { + moveTo(scaled(17f), scaled(9f)) + lineTo(scaled(22f), scaled(7f)) + lineTo(scaled(22f), scaled(17f)) + lineTo(scaled(17f), scaled(15f)) + close() + } + drawPath(triangle, c) +} + +private fun DrawScope.drawBook(c: Color, s: Stroke) { + val p = Path().apply { + moveTo(scaled(4f), scaled(5f)) + lineTo(scaled(12f), scaled(7f)) + lineTo(scaled(20f), scaled(5f)) + lineTo(scaled(20f), scaled(19f)) + lineTo(scaled(12f), scaled(21f)) + lineTo(scaled(4f), scaled(19f)) + close() + moveTo(scaled(12f), scaled(7f)) + lineTo(scaled(12f), scaled(21f)) + } + drawPath(p, c, style = s) +} + +private fun DrawScope.drawManga(c: Color, s: Stroke) { + drawRect(color = c, topLeft = Offset(scaled(4f), scaled(4f)), size = Size(scaled(7f), scaled(16f)), style = s) + drawRect(color = c, topLeft = Offset(scaled(13f), scaled(4f)), size = Size(scaled(7f), scaled(16f)), style = s) +} + +private fun DrawScope.drawKey(c: Color, s: Stroke) { + drawCircle(color = c, radius = scaled(3.2f), center = Offset(scaled(8f), scaled(12f)), style = s) + val teeth = Path().apply { + moveTo(scaled(11.2f), scaled(12f)) + lineTo(scaled(20f), scaled(12f)) + lineTo(scaled(20f), scaled(16f)) + moveTo(scaled(17f), scaled(12f)) + lineTo(scaled(17f), scaled(14.5f)) + } + drawPath(teeth, c, style = s) +} + +private fun DrawScope.drawAudio(c: Color) { + drawRect(color = c, topLeft = Offset(scaled(4f), scaled(10f)), size = Size(scaled(2.5f), scaled(8f))) + drawRect(color = c, topLeft = Offset(scaled(10.75f), scaled(6f)), size = Size(scaled(2.5f), scaled(14f))) + drawRect(color = c, topLeft = Offset(scaled(17.5f), scaled(3f)), size = Size(scaled(2.5f), scaled(18f))) +} + +private fun DrawScope.drawBackup(c: Color, s: Stroke) { + val arrow = Path().apply { + moveTo(scaled(12f), scaled(4f)) + lineTo(scaled(12f), scaled(14f)) + moveTo(scaled(8f), scaled(11f)) + lineTo(scaled(12f), scaled(15f)) + lineTo(scaled(16f), scaled(11f)) + } + drawPath(arrow, c, style = s) + val tray = Path().apply { + moveTo(scaled(4f), scaled(17f)) + lineTo(scaled(4f), scaled(20f)) + lineTo(scaled(20f), scaled(20f)) + lineTo(scaled(20f), scaled(17f)) + } + drawPath(tray, c, style = s) +} + +private fun DrawScope.drawReader(c: Color, s: Stroke) { + val p = Path().apply { + moveTo(scaled(6f), scaled(4f)) + lineTo(scaled(18f), scaled(4f)) + lineTo(scaled(18f), scaled(20f)) + lineTo(scaled(12f), scaled(17f)) + lineTo(scaled(6f), scaled(20f)) + close() + } + drawPath(p, c, style = s) +} + +private fun DrawScope.drawCrossPlatform(c: Color, s: Stroke) { + drawRoundRect( + color = c, + topLeft = Offset(scaled(3f), scaled(3f)), + size = Size(scaled(11f), scaled(11f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), + style = s, + ) + drawRoundRect( + color = c, + topLeft = Offset(scaled(10f), scaled(10f)), + size = Size(scaled(11f), scaled(11f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), + ) +} + +private fun DrawScope.drawCloud(c: Color, s: Stroke) { + val p = Path().apply { + moveTo(scaled(7f), scaled(17f)) + cubicTo(scaled(3f), scaled(17f), scaled(3f), scaled(9f), scaled(7f), scaled(9f)) + cubicTo(scaled(7f), scaled(4f), scaled(17f), scaled(4f), scaled(17f), scaled(10f)) + cubicTo(scaled(20.5f), scaled(10f), scaled(20.5f), scaled(17f), scaled(17f), scaled(17f)) + close() + } + drawPath(p, c, style = s) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt new file mode 100644 index 000000000..4b304dcfd --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt @@ -0,0 +1,66 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalStatusColors + +/** Patch (single dot) / Minor (two dots) / Major (bar + slash). */ +enum class VersionDeltaKind { PATCH, MINOR, MAJOR } + +/** + * Visual update-risk indicator. Replaces "v2.7.0 → v2.7.5" prose with a primitive + * (DESIGN.md §4.1). PATCH = green dot, MINOR = two amber dots, MAJOR = filled bar + + * slash. Pairs with [VersionStack] for "how far behind" magnitude. + */ +@Composable +fun VersionDelta( + delta: VersionDeltaKind, + modifier: Modifier = Modifier, +) { + val status = LocalStatusColors.current + when (delta) { + VersionDeltaKind.PATCH -> Box( + modifier = modifier + .size(6.dp) + .background(status.freshnessFresh, CircleShape), + ) + VersionDeltaKind.MINOR -> Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(2.5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(2) { + Box( + modifier = Modifier + .size(6.dp) + .background(status.freshnessWarm, CircleShape), + ) + } + } + VersionDeltaKind.MAJOR -> Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(width = 9.dp, height = 9.dp) + .background(status.freshnessHot, RoundedCornerShape(1.5.dp)), + ) + Box( + modifier = Modifier + .size(width = 2.dp, height = 9.dp) + .background(status.freshnessHot), + ) + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt new file mode 100644 index 000000000..71854f5bb --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt @@ -0,0 +1,43 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalStatusColors +import kotlin.math.min + +/** + * Stack of bars — one per skipped release. Grows tall with distance from current, + * capped at 7. Replaces "5 versions behind" prose (DESIGN.md §4.1). + */ +@Composable +fun VersionStack( + count: Int, + modifier: Modifier = Modifier, + color: Color? = null, + widthDp: Int = 22, +) { + val fillColor = color ?: LocalStatusColors.current.freshnessWarm + val n = min(count, 7) + if (n == 0) return + val heightDp = n * 3 + 5 + Canvas(modifier = modifier.size(width = widthDp.dp, height = heightDp.dp)) { + val barWidth = size.width + val barHeight = 4.dp.toPx() + val gap = 3.dp.toPx() + val barRadius = 1.5.dp.toPx() + for (i in 0 until n) { + val top = i * gap + val alpha = 0.35f + i * 0.08f + drawRoundRect( + color = fillColor.copy(alpha = alpha), + topLeft = androidx.compose.ui.geometry.Offset(0f, top), + size = androidx.compose.ui.geometry.Size(barWidth, barHeight), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(barRadius, barRadius), + ) + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt new file mode 100644 index 000000000..db5f49295 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt @@ -0,0 +1,104 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.LocalStatusColors + +/** Signing-fingerprint trust state. */ +enum class WaxSealState { INTACT, CRACKED, OPEN } + +/** + * Wax-stamp glyph for binary trust (DESIGN.md §7.8). INTACT = solid brown circle + + * inner check; CRACKED = red circle + lightning split (the ONLY aggressive red); + * OPEN = dashed grey ring (signing-fingerprint unknown). 22dp default; 36-44dp on + * Detail trust card. + */ +@Composable +fun WaxSeal( + state: WaxSealState, + modifier: Modifier = Modifier, + sizeDp: Int = 22, +) { + val status = LocalStatusColors.current + Canvas(modifier = modifier.size(sizeDp.dp)) { + val s = size.minDimension + val r = s * (9.5f / 24f) + val center = Offset(size.width / 2f, size.height / 2f) + when (state) { + WaxSealState.INTACT -> { + drawCircle(color = status.waxIntact, radius = r, center = center) + drawCircle( + color = Color(0xFF5E2F18), + radius = r, + center = center, + style = Stroke(width = 0.5f.dp.toPx()), + ) + drawCircle( + color = Color(0xFFFCE8C8).copy(alpha = 0.55f), + radius = s * (6f / 24f), + center = center, + style = Stroke(width = 0.6f.dp.toPx()), + ) + // Inner check mark + val path = Path().apply { + moveTo(size.width * (8.5f / 24f), size.height * (12f / 24f)) + lineTo(size.width * (11f / 24f), size.height * (14.3f / 24f)) + lineTo(size.width * (15.5f / 24f), size.height * (9.7f / 24f)) + } + drawPath( + path = path, + color = Color(0xFFFCE8C8), + style = Stroke( + width = 1.8f.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round, + ), + ) + } + WaxSealState.CRACKED -> { + drawCircle(color = status.waxCracked, radius = r, center = center) + val crackPath = Path().apply { + moveTo(size.width * (9f / 24f), size.height * (4f / 24f)) + lineTo(size.width * (13f / 24f), size.height * (11f / 24f)) + lineTo(size.width * (8f / 24f), size.height * (14f / 24f)) + lineTo(size.width * (14f / 24f), size.height * (20f / 24f)) + } + drawPath( + path = crackPath, + color = Color(0xFFFBE2DD), + style = Stroke( + width = 2f.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round, + ), + ) + } + WaxSealState.OPEN -> { + val dashEffect = PathEffect.dashPathEffect( + intervals = floatArrayOf(2.5f.dp.toPx(), 2f.dp.toPx()), + ) + drawCircle( + color = status.waxOpen, + radius = s * (9f / 24f), + center = center, + style = Stroke(width = 1.6f.dp.toPx(), pathEffect = dashEffect), + ) + drawCircle( + color = status.waxOpen.copy(alpha = 0.5f), + radius = s * (2f / 24f), + center = center, + ) + } + } + } +} From 7c530f9d36b1a6b872757650c9ea21cd5625f7b6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:21:51 +0500 Subject: [PATCH 009/172] feat(components): WonkySquircleShape + 4 button variants --- .../components/buttons/IconButton.kt | 34 ++++ .../components/buttons/OutlineButton.kt | 56 ++++++ .../components/buttons/PrimaryButton.kt | 80 ++++++++ .../components/buttons/TintedButton.kt | 54 ++++++ .../theme/shapes/WonkySquircleShape.kt | 171 ++++++++++++++++++ 5 files changed, 395 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt new file mode 100644 index 000000000..601391786 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt @@ -0,0 +1,34 @@ +package zed.rainxch.core.presentation.components.buttons + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +/** + * 36×36 transparent icon button. Back / Share / Favorite / More (DESIGN.md §7.1). + * Min 48dp touch target preserved via padding inside the click area. + */ +@Composable +fun IconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + sizeDp: Int = 36, + content: @Composable () -> Unit, +) { + Box( + modifier = modifier + .clip(CircleShape) + .clickable(enabled = enabled, onClick = onClick) + .size(sizeDp.dp.coerceAtLeast(48.dp)), + contentAlignment = Alignment.Center, + ) { + content() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt new file mode 100644 index 000000000..9cf00b7ad --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt @@ -0,0 +1,56 @@ +package zed.rainxch.core.presentation.components.buttons + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +/** + * Outline tertiary action — transparent fill, 1dp outline ring, pill-shaped. + * Inspect / Refresh / Filter / Cancel (DESIGN.md §7.1). + */ +@Composable +fun OutlineButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable () -> Unit, +) { + val pillShape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(pillShape) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = if (enabled) 1f else 0.38f), + shape = pillShape, + ) + .clickable(enabled = enabled, onClick = onClick) + .heightIn(min = 40.dp) + .padding(horizontal = 14.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurface.copy( + alpha = if (enabled) 1f else 0.38f, + ), + ) { + ProvideTextStyle(MaterialTheme.typography.labelLarge) { + content() + } + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt new file mode 100644 index 000000000..413db5f2c --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt @@ -0,0 +1,80 @@ +package zed.rainxch.core.presentation.components.buttons + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape + +/** + * Primary CTA — wonky asymmetric squircle filled with `primary`. Reserved for the + * single most important action on a surface (DESIGN.md §7.1): Install / Update / + * Open / Get / Sign in. Spring-physics press feedback per D10. + */ +@Composable +fun PrimaryButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: WonkySquircleShape = WonkySquircleShape.CtaPrimary, + backgroundColor: Color = MaterialTheme.colorScheme.primary, + contentColor: Color = Color.White, + content: @Composable () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed) 0.96f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessHigh), + label = "primary-button-press", + ) + Row( + modifier = modifier + .scale(scale) + .clip(shape) + .background(if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.38f)) + .clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + .heightIn(min = 48.dp) + .padding(horizontal = 20.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider(LocalContentColor provides contentColor) { + ProvideTextStyle( + value = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp, + ), + ) { + content() + } + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt new file mode 100644 index 000000000..ef0a64110 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt @@ -0,0 +1,54 @@ +package zed.rainxch.core.presentation.components.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Tinted secondary CTA — `primaryContainer` background, `primary` text. Asymmetric + * (non-wonky) squircle. Used for Get / Read more / secondary install-panel actions + * (DESIGN.md §7.1). + */ +@Composable +fun TintedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + backgroundColor: Color = MaterialTheme.colorScheme.primaryContainer, + contentColor: Color = MaterialTheme.colorScheme.primary, + content: @Composable () -> Unit, +) { + Row( + modifier = modifier + .clip(Radii.cardSm) + .background(if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.38f)) + .clickable(enabled = enabled, onClick = onClick) + .heightIn(min = 44.dp) + .padding(horizontal = 16.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider(LocalContentColor provides contentColor) { + ProvideTextStyle( + value = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), + ) { + content() + } + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt new file mode 100644 index 000000000..650f989f0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt @@ -0,0 +1,171 @@ +package zed.rainxch.core.presentation.theme.shapes + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** + * Hand-shaped asymmetric corner with elliptical (x ≠ y) radii — the "wonky squircle" + * from DESIGN.md §5.2 / §7.7 / §16.x. Compose's `RoundedCornerShape` cannot express + * elliptical corners (it's circular only), so this `Shape` builds a [Path] manually + * using `arcTo` with rectangular bounds whose width and height are independent. + * + * Each corner takes `(rxDp, ryDp)` — the horizontal and vertical sweep of that corner's + * ellipse arc. The CSS notation `border-radius: 20 14 22 16 / 16 22 14 20` translates to: + * topStart = (20, 16) + * topEnd = (14, 22) + * bottomEnd = (22, 14) + * bottomStart = (16, 20) + * + * Three preset constants ([CtaPrimary], [CtaAlt], [Search]) match the tokens.json + * `shape.wonkySquircle` block. For ad-hoc corners (sheets / dialogs / toasts), use + * the [WonkySquircleShape] constructor directly. + */ +class WonkySquircleShape( + private val topStart: CornerRadii, + private val topEnd: CornerRadii, + private val bottomEnd: CornerRadii, + private val bottomStart: CornerRadii, +) : Shape { + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + with(density) { + val tsX = topStart.rx.toPx().coerceAtMost(size.width / 2f) + val tsY = topStart.ry.toPx().coerceAtMost(size.height / 2f) + val teX = topEnd.rx.toPx().coerceAtMost(size.width / 2f) + val teY = topEnd.ry.toPx().coerceAtMost(size.height / 2f) + val beX = bottomEnd.rx.toPx().coerceAtMost(size.width / 2f) + val beY = bottomEnd.ry.toPx().coerceAtMost(size.height / 2f) + val bsX = bottomStart.rx.toPx().coerceAtMost(size.width / 2f) + val bsY = bottomStart.ry.toPx().coerceAtMost(size.height / 2f) + + val path = Path().apply { + // Start at top-start corner end-point (after sweeping in) + moveTo(tsX, 0f) + // Top edge → top-end corner + lineTo(size.width - teX, 0f) + arcToCorner( + cornerCenter = Offset(size.width - teX, teY), + radiusX = teX, + radiusY = teY, + startAngle = 270f, + sweep = 90f, + ) + // Right edge → bottom-end corner + lineTo(size.width, size.height - beY) + arcToCorner( + cornerCenter = Offset(size.width - beX, size.height - beY), + radiusX = beX, + radiusY = beY, + startAngle = 0f, + sweep = 90f, + ) + // Bottom edge → bottom-start corner + lineTo(bsX, size.height) + arcToCorner( + cornerCenter = Offset(bsX, size.height - bsY), + radiusX = bsX, + radiusY = bsY, + startAngle = 90f, + sweep = 90f, + ) + // Left edge → top-start corner + lineTo(0f, tsY) + arcToCorner( + cornerCenter = Offset(tsX, tsY), + radiusX = tsX, + radiusY = tsY, + startAngle = 180f, + sweep = 90f, + ) + close() + } + return Outline.Generic(path) + } + } + + companion object { + /** `tokens.json.shape.wonkySquircle.css`: 20 14 22 16 / 16 22 14 20 */ + val CtaPrimary = WonkySquircleShape( + topStart = CornerRadii(20.dp, 16.dp), + topEnd = CornerRadii(14.dp, 22.dp), + bottomEnd = CornerRadii(22.dp, 14.dp), + bottomStart = CornerRadii(16.dp, 20.dp), + ) + + /** `tokens.json.shape.wonkySquircle.alt`: 22 16 24 18 / 18 24 16 22 */ + val CtaAlt = WonkySquircleShape( + topStart = CornerRadii(22.dp, 18.dp), + topEnd = CornerRadii(16.dp, 24.dp), + bottomEnd = CornerRadii(24.dp, 16.dp), + bottomStart = CornerRadii(18.dp, 22.dp), + ) + + /** `tokens.json.shape.wonkySquircle.search`: 24 18 26 20 / 18 24 20 26 */ + val Search = WonkySquircleShape( + topStart = CornerRadii(24.dp, 18.dp), + topEnd = CornerRadii(18.dp, 24.dp), + bottomEnd = CornerRadii(26.dp, 20.dp), + bottomStart = CornerRadii(20.dp, 26.dp), + ) + + /** Sheet — square bottom corners (flush to screen edge). */ + val Sheet = WonkySquircleShape( + topStart = CornerRadii(24.dp, 18.dp), + topEnd = CornerRadii(18.dp, 24.dp), + bottomEnd = CornerRadii(0.dp, 0.dp), + bottomStart = CornerRadii(0.dp, 0.dp), + ) + + /** Dialog — symmetric-ish wonky. */ + val Dialog = WonkySquircleShape( + topStart = CornerRadii(28.dp, 22.dp), + topEnd = CornerRadii(22.dp, 28.dp), + bottomEnd = CornerRadii(26.dp, 24.dp), + bottomStart = CornerRadii(24.dp, 26.dp), + ) + + /** Toast — compact wonky. */ + val Toast = WonkySquircleShape( + topStart = CornerRadii(18.dp, 14.dp), + topEnd = CornerRadii(14.dp, 22.dp), + bottomEnd = CornerRadii(22.dp, 16.dp), + bottomStart = CornerRadii(16.dp, 18.dp), + ) + } +} + +data class CornerRadii(val rx: Dp, val ry: Dp) + +private fun Path.arcToCorner( + cornerCenter: Offset, + radiusX: Float, + radiusY: Float, + startAngle: Float, + sweep: Float, +) { + if (radiusX == 0f || radiusY == 0f) return + val bounds = Rect( + left = cornerCenter.x - radiusX, + top = cornerCenter.y - radiusY, + right = cornerCenter.x + radiusX, + bottom = cornerCenter.y + radiusY, + ) + arcTo( + rect = bounds, + startAngleDegrees = startAngle, + sweepAngleDegrees = sweep, + forceMoveTo = false, + ) +} From e4c5b55483fddccdf3b6823396ff4e759bf262d9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:23:54 +0500 Subject: [PATCH 010/172] feat(components): chips, section header, banner, accent resolver --- .../presentation/components/chips/AddChip.kt | 75 +++++++++++ .../components/chips/FilterChip.kt | 68 ++++++++++ .../presentation/components/section/Banner.kt | 54 ++++++++ .../components/section/SectionHeader.kt | 72 +++++++++++ .../core/presentation/vocabulary/AppAccent.kt | 119 ++++++++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt new file mode 100644 index 000000000..8edf221c5 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt @@ -0,0 +1,75 @@ +package zed.rainxch.core.presentation.components.chips + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Dashed "+ Add filter" affordance (DESIGN.md §7.2). Same dimensions as [FilterChip] + * but with a dashed outline instead of solid. + */ +@Composable +fun AddChip( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val borderColor = MaterialTheme.colorScheme.outline + Row( + modifier = modifier + .clip(Radii.chip) + .clickable(onClick = onClick) + .drawWithCache { + val stroke = Stroke( + width = 1.dp.toPx(), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(4f.dp.toPx(), 3f.dp.toPx())), + ) + onDrawWithContent { + drawContent() + drawRoundRect( + color = borderColor, + cornerRadius = androidx.compose.ui.geometry.CornerRadius( + x = 11.dp.toPx(), + y = 8.dp.toPx(), + ), + style = stroke, + ) + } + } + .padding(horizontal = 12.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "+", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = label, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + ), + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt new file mode 100644 index 000000000..4bff7fc0b --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt @@ -0,0 +1,68 @@ +package zed.rainxch.core.presentation.components.chips + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Active/inactive filter chip (DESIGN.md §7.2). Active = tintP bg + primary text + + * 1dp primary-tinted border. Inactive = transparent + outline border + ink text. + * Optional `×` chip — caller composes via dismiss arg. + */ +@Composable +fun FilterChip( + label: String, + active: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + onDismiss: (() -> Unit)? = null, +) { + val bg: Color = if (active) MaterialTheme.colorScheme.primaryContainer else Color.Transparent + val fg: Color = if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface + val border: Color = if (active) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.33f) + } else { + MaterialTheme.colorScheme.outline + } + Row( + modifier = modifier + .clip(Radii.chip) + .background(bg) + .border(width = 1.dp, color = border, shape = Radii.chip) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + color = fg, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), + ) + if (active && onDismiss != null) { + Text( + text = "×", + color = fg, + modifier = Modifier.clickable(onClick = onDismiss), + fontSize = 14.sp, + ) + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt new file mode 100644 index 000000000..52ce732e2 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt @@ -0,0 +1,54 @@ +package zed.rainxch.core.presentation.components.section + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** Banner tint variants per DESIGN.md §7.6. */ +enum class BannerTint { Info, Success, Warning, Danger } + +/** + * Inline banner for clipboard detection, update available, integrity warnings, etc. + * (DESIGN.md §7.6). Composes glyph + body + optional trailing action via slot APIs. + */ +@Composable +fun Banner( + tint: BannerTint = BannerTint.Info, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + content: @Composable () -> Unit, +) { + val cs = MaterialTheme.colorScheme + val (bg, border) = when (tint) { + BannerTint.Info -> cs.primaryContainer to cs.primary.copy(alpha = 0.2f) + BannerTint.Success -> cs.tertiaryContainer to cs.tertiary.copy(alpha = 0.2f) + BannerTint.Warning -> Color(0xFFFFE6CC) to Color(0xFFC49652).copy(alpha = 0.4f) + BannerTint.Danger -> cs.errorContainer to cs.error.copy(alpha = 0.2f) + } + Row( + modifier = modifier + .clip(Radii.card) + .background(bg) + .border(width = 1.dp, color = border, shape = Radii.card) + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + leading?.invoke() + androidx.compose.foundation.layout.Box(modifier = Modifier.weight(1f)) { + content() + } + trailing?.invoke() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt new file mode 100644 index 000000000..91b777c6a --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt @@ -0,0 +1,72 @@ +package zed.rainxch.core.presentation.components.section + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.vocabulary.Squiggle + +/** + * Section header with leading glyph + Fraunces italic title + optional sub-count + + * Squiggle underline + optional "See all ›" affordance (DESIGN.md §7.5). + */ +@Composable +fun SectionHeader( + title: String, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + subCount: String? = null, + onSeeAll: (() -> Unit)? = null, +) { + Column( + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + leading?.invoke() + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + if (subCount != null) { + Text( + text = "· $subCount", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + if (onSeeAll != null) { + Text( + text = "See all ›", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + modifier = Modifier + .clickable(onClick = onSeeAll) + .padding(start = 4.dp), + ) + } + } + Squiggle() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt new file mode 100644 index 000000000..a74b0aad0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt @@ -0,0 +1,119 @@ +package zed.rainxch.core.presentation.vocabulary + +import androidx.compose.ui.graphics.Color + +/** + * Per-app accent triple (DESIGN.md §2.4). The accent travels with the repo across + * surfaces (lead card bg tint, freshness ring outer, install panel bloom). + * + * - `c` — saturated accent (the brand color) + * - `lt` — light tint (used in all light palettes as a 18–22% bg) + * - `dtAlpha` — alpha applied to `c` over the dark surface (22% per themes.md) + */ +data class AppAccent(val c: Color, val lt: Color, val dtAlpha: Float = 0.22f) { + fun tintFor(isDark: Boolean): Color = if (isDark) c.copy(alpha = dtAlpha) else lt +} + +/** + * Resolves a per-app accent from (in order): + * 1. Backend-supplied hex (when [backendHex] is non-null and parseable) + * 2. Topic-derived (first match in [TOPIC_ACCENTS]) + * 3. Language-derived ([LANGUAGE_ACCENTS]) + * 4. Blue fallback (Nord primary) + * + * Source-of-truth tables in UI-SPEC.md §6.1 / §6.2. Deterministic — same repo + * resolves to the same accent across launches and devices. + */ +object AppAccentResolver { + private val FALLBACK = AppAccent(c = Color(0xFF5E81AC), lt = Color(0xFFD8E1EC)) + + private val TOPIC_ACCENTS: Map = buildMap { + // Each pair is (canonical topic name → accent). Aliases listed inline. + val photo = AppAccent(Color(0xFF5E81AC), Color(0xFFD8E1EC)) + listOf("photo", "photos", "gallery").forEach { put(it, photo) } + + val manga = AppAccent(Color(0xFF7E6BA8), Color(0xFFDCD7E7)) + listOf("manga", "comic", "reader").forEach { put(it, manga) } + + val security = AppAccent(Color(0xFF4C6E96), Color(0xFFCFDAE7)) + listOf("password-manager", "security", "vault").forEach { put(it, security) } + + val audio = AppAccent(Color(0xFF6B8E5A), Color(0xFFDCE7CE)) + listOf("podcast", "audio", "music").forEach { put(it, audio) } + + val book = AppAccent(Color(0xFF9B6B3C), Color(0xFFEDD9BB)) + listOf("book", "ebook", "koreader").forEach { put(it, book) } + + val messaging = AppAccent(Color(0xFFA35365), Color(0xFFEFCDD3)) + listOf("messaging", "chat", "signal").forEach { put(it, messaging) } + + val vpn = AppAccent(Color(0xFF5C7A8E), Color(0xFFD5DEE5)) + listOf("vpn", "network", "proxy").forEach { put(it, vpn) } + + val notes = AppAccent(Color(0xFF7A6549), Color(0xFFE5D9C6)) + listOf("note", "notes", "markdown").forEach { put(it, notes) } + + val backup = AppAccent(Color(0xFF5A6A57), Color(0xFFCBD3C9)) + listOf("backup", "sync").forEach { put(it, backup) } + + val selfHosted = AppAccent(Color(0xFF356859), Color(0xFFB8E0D2)) + listOf("self-hosted", "home-server").forEach { put(it, selfHosted) } + + val video = AppAccent(Color(0xFFB8542C), Color(0xFFFFE7CB)) + listOf("video", "media").forEach { put(it, video) } + } + + private val LANGUAGE_ACCENTS: Map = mapOf( + "Kotlin" to AppAccent(Color(0xFF7E6BA8), Color(0xFFDCD7E7)), + "Java" to AppAccent(Color(0xFFB8542C), Color(0xFFFFE7CB)), + "TypeScript" to AppAccent(Color(0xFF5E81AC), Color(0xFFD8E1EC)), + "JavaScript" to AppAccent(Color(0xFF5E81AC), Color(0xFFD8E1EC)), + "Python" to AppAccent(Color(0xFF356859), Color(0xFFB8E0D2)), + "Rust" to AppAccent(Color(0xFFA35346), Color(0xFFEDCFC9)), + "Go" to AppAccent(Color(0xFF5C7A8E), Color(0xFFD5DEE5)), + "C" to AppAccent(Color(0xFF7A6549), Color(0xFFE5D9C6)), + "C++" to AppAccent(Color(0xFF7A6549), Color(0xFFE5D9C6)), + "Swift" to AppAccent(Color(0xFFB8542C), Color(0xFFFFE7CB)), + "Dart" to AppAccent(Color(0xFF5E81AC), Color(0xFFD8E1EC)), + "Ruby" to AppAccent(Color(0xFFB83A2C), Color(0xFFF3D7CF)), + "Shell" to AppAccent(Color(0xFF6B8E5A), Color(0xFFDCE7CE)), + "Bash" to AppAccent(Color(0xFF6B8E5A), Color(0xFFDCE7CE)), + ) + + fun resolve( + backendHex: String? = null, + topics: List = emptyList(), + primaryLanguage: String? = null, + ): AppAccent { + // 1. Backend + backendHex?.let { parseHexAccent(it) }?.let { return it } + // 2. Topic + topics.forEach { t -> + TOPIC_ACCENTS[t.lowercase()]?.let { return it } + } + // 3. Language + primaryLanguage?.let { LANGUAGE_ACCENTS[it] }?.let { return it } + // 4. Fallback + return FALLBACK + } + + private fun parseHexAccent(hex: String): AppAccent? { + val cleaned = hex.removePrefix("#").trim() + if (cleaned.length != 6 && cleaned.length != 8) return null + val rgb = runCatching { + val intVal = cleaned.toLong(16) + if (cleaned.length == 6) (0xFF000000 or intVal).toULong() else intVal.toULong() + }.getOrNull() ?: return null + val c = Color(rgb.toLong()) + // Synthetic light tint = c blended toward white at 78% + val lt = blendTowardWhite(c, 0.78f) + return AppAccent(c, lt) + } + + private fun blendTowardWhite(c: Color, factor: Float): Color = Color( + red = c.red + (1f - c.red) * factor, + green = c.green + (1f - c.green) * factor, + blue = c.blue + (1f - c.blue) * factor, + alpha = c.alpha, + ) +} From a2f8e7584b7efdb5dc60325f0750e8f360d23321 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:25:02 +0500 Subject: [PATCH 011/172] feat(components): cards (lead/compact/row), vital signs, wax-seal trust --- .../components/cards/CompactCard.kt | 37 +++++++ .../components/cards/LeadHeroCard.kt | 56 ++++++++++ .../presentation/components/cards/RowCard.kt | 38 +++++++ .../components/cards/VitalSignsGrid.kt | 101 ++++++++++++++++++ .../components/cards/WaxSealTrustCard.kt | 77 +++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt new file mode 100644 index 000000000..db0a81140 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt @@ -0,0 +1,37 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Compact card archetype — surface bg, dashed-border footer (composed by caller), + * card-sized asymmetric squircle (DESIGN.md §7.3). Used by Hot release cards, + * Trending cards, etc. Caller supplies internal layout via slot. + */ +@Composable +fun CompactCard( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val cs = MaterialTheme.colorScheme + Column( + modifier = modifier + .clip(Radii.card) + .background(cs.surface) + .border(width = 1.dp, color = cs.outline, shape = Radii.card) + .let { if (onClick != null) it.clickable(onClick = onClick) else it } + .padding(14.dp), + ) { + content() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt new file mode 100644 index 000000000..c7dcb9982 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt @@ -0,0 +1,56 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.vocabulary.AppAccent + +/** + * Lead/hero card — full-width, accent-tinted bg, soft radial accent bloom, wonky + * squircle shape (DESIGN.md §7.3). Used for the top Hot release card on Home and + * featured items. Bloom uses [accent.c] at low alpha; DESIGN.md §2.5 explicitly + * permits this as editorial flair. + */ +@Composable +fun LeadHeroCard( + accent: AppAccent, + isDark: Boolean, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val cs = MaterialTheme.colorScheme + val baseTint = accent.tintFor(isDark) + Box( + modifier = modifier + .clip(WonkySquircleShape.CtaPrimary) + .background(if (isDark) cs.surface else baseTint) + .let { if (onClick != null) it.clickable(onClick = onClick) else it }, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.radialGradient( + colors = listOf(accent.c.copy(alpha = 0.14f), accent.c.copy(alpha = 0f)), + center = Offset(x = 200f, y = 60f), + radius = 480f, + ), + ), + ) + Column(modifier = Modifier.padding(20.dp)) { + content() + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt new file mode 100644 index 000000000..638f0701a --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt @@ -0,0 +1,38 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Dense list row — surface bg + squircle radius. Used by Library, Most Popular, + * Search results (DESIGN.md §7.4). 10–12dp padding, 11dp gap. Caller supplies + * row content via slot. + */ +@Composable +fun RowCard( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + Row( + modifier = modifier + .clip(Radii.row) + .background(MaterialTheme.colorScheme.surface) + .let { if (onClick != null) it.clickable(onClick = onClick) else it } + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(11.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt new file mode 100644 index 000000000..40a2316a3 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt @@ -0,0 +1,101 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * 2×2 vital-signs grid on Detail pages (DESIGN.md §7.7). Fixed slots: + * Released · Maintained · Stars · Permissions. + * + * Each [VitalTile] takes glyph + value (signal-colored Fraunces italic 13) + label. + */ +@Composable +fun VitalSignsGrid( + released: VitalTile, + maintained: VitalTile, + stars: VitalTile, + permissions: VitalTile, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + VitalTileBox(released, Modifier.weight(1f)) + VitalTileBox(maintained, Modifier.weight(1f)) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + VitalTileBox(stars, Modifier.weight(1f)) + VitalTileBox(permissions, Modifier.weight(1f)) + } + } +} + +data class VitalTile( + val label: String, + val value: String, + val valueColor: Color? = null, + val glyph: @Composable () -> Unit = {}, +) + +@Composable +private fun VitalTileBox(tile: VitalTile, modifier: Modifier = Modifier) { + val cs = MaterialTheme.colorScheme + Column( + modifier = modifier + .clip(Radii.cardSm) + .background(cs.surfaceContainer) + .border(width = 1.dp, color = cs.outline, shape = Radii.cardSm) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Box(modifier = Modifier, contentAlignment = Alignment.Center) { + tile.glyph() + } + Text( + text = tile.value, + style = MaterialTheme.typography.bodyLarge.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + ), + color = tile.valueColor ?: cs.onSurface, + ) + Text( + text = tile.label.uppercase(), + color = cs.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 9.5.sp, + letterSpacing = 0.04.em(), + ), + ) + } +} + +private fun Double.em() = androidx.compose.ui.unit.TextUnit(this.toFloat(), androidx.compose.ui.unit.TextUnitType.Em) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt new file mode 100644 index 000000000..ba5b22d26 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt @@ -0,0 +1,77 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.jetbrainsMono +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.WaxSeal +import zed.rainxch.core.presentation.vocabulary.WaxSealState + +/** + * Trust card anchored to the install panel (DESIGN.md §7.8). Wax-seal glyph + + * Fraunces italic state label + JetBrains Mono fingerprint detail. Backgrounds + * follow the seal state — successT tint for intact, dangerT for cracked, surface + * for open. The cracked state is the ONLY place red is aggressive in the UI. + */ +@Composable +fun WaxSealTrustCard( + state: WaxSealState, + fingerprintDetail: String, + modifier: Modifier = Modifier, + stateLabel: String = defaultLabel(state), +) { + val cs = MaterialTheme.colorScheme + val (bg, border) = when (state) { + WaxSealState.INTACT -> cs.tertiaryContainer to cs.tertiary + WaxSealState.CRACKED -> cs.errorContainer to cs.error + WaxSealState.OPEN -> cs.surface to cs.outline + } + Row( + modifier = modifier + .clip(Radii.card) + .background(bg) + .border(width = 1.dp, color = border, shape = Radii.card) + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + WaxSeal(state = state, sizeDp = 38) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stateLabel, + style = MaterialTheme.typography.titleMedium.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + ), + color = cs.onSurface, + ) + Text( + text = fingerprintDetail, + fontFamily = jetbrainsMono, + color = cs.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp), + ) + } + } +} + +private fun defaultLabel(state: WaxSealState): String = when (state) { + WaxSealState.INTACT -> "Sealed" + WaxSealState.CRACKED -> "Broken seal" + WaxSealState.OPEN -> "Unsigned" +} From a8bce329ee425e58f40b0d0f837bcf33cdd77862 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:26:27 +0500 Subject: [PATCH 012/172] feat(components): overlays (sheet, dialog, toast, fullscreen, dropdown) --- .../components/overlays/GhsBottomSheet.kt | 71 +++++++++++++++ .../components/overlays/GhsConfirmDialog.kt | 88 +++++++++++++++++++ .../components/overlays/GhsDropdownMenu.kt | 69 +++++++++++++++ .../components/overlays/GhsFullScreenSheet.kt | 56 ++++++++++++ .../components/overlays/GhsToast.kt | 56 ++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt new file mode 100644 index 000000000..cbe8df036 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt @@ -0,0 +1,71 @@ +package zed.rainxch.core.presentation.components.overlays + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape + +/** + * Wonky-top bottom sheet (DESIGN.md §16.1). Wraps Material 3's [ModalBottomSheet] + * with the project's asymmetric top corners + drag handle. + * + * Caller composes title + content + action row via slots. Use right-aligned primary + * (wonky) + outline Cancel pair in the action row. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GhsBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + dragHandle: @Composable (() -> Unit)? = { DefaultDragHandle() }, + content: @Composable () -> Unit, +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + modifier = modifier, + shape = WonkySquircleShape.Sheet, + containerColor = MaterialTheme.colorScheme.surface, + scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f), + dragHandle = dragHandle, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 20.dp), + ) { + content() + } + } +} + +@Composable +private fun DefaultDragHandle() { + Box( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + contentAlignment = Alignment.TopCenter, + ) { + Box( + modifier = Modifier + .size(width = 36.dp, height = 4.dp) + .clip(RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)), + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt new file mode 100644 index 000000000..3a8835098 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt @@ -0,0 +1,88 @@ +package zed.rainxch.core.presentation.components.overlays + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.vocabulary.Squiggle + +/** + * Modal confirm dialog (DESIGN.md §16.2). Cancel-left, Confirm-right convention. + * Destructive confirms pass `destructive = true` to swap the primary fill to + * danger color. + */ +@Composable +fun GhsConfirmDialog( + title: String, + body: String, + confirmLabel: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + cancelLabel: String = "Cancel", + destructive: Boolean = false, + leading: (@Composable () -> Unit)? = null, +) { + Dialog(onDismissRequest = onDismiss) { + val cs = MaterialTheme.colorScheme + Column( + modifier = Modifier + .widthIn(max = 400.dp) + .clip(WonkySquircleShape.Dialog) + .background(cs.surface) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (leading != null) { + Box(modifier = Modifier.padding(bottom = 4.dp)) { leading() } + } + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = cs.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = body, + color = cs.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + Squiggle() + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlineButton(onClick = onDismiss) { Text(cancelLabel) } + PrimaryButton( + onClick = onConfirm, + backgroundColor = if (destructive) cs.error else cs.primary, + ) { Text(confirmLabel) } + } + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt new file mode 100644 index 000000000..0ae1a89b2 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt @@ -0,0 +1,69 @@ +package zed.rainxch.core.presentation.components.overlays + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +/** + * Floating dropdown panel (DESIGN.md §16.7). Surface bg, wonky-soft squircle, 1dp + * outline, soft shadow. Items composed by caller using [GhsDropdownItem]. + */ +@Composable +fun GhsDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier + .clip(Radii.cardSm) + .background(MaterialTheme.colorScheme.surface) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = Radii.cardSm), + ) { + Column(content = { content() }) + } +} + +@Composable +fun GhsDropdownItem( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, +) { + Row( + modifier = modifier + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + leading?.invoke() + Text( + text = label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + if (trailing != null) { + androidx.compose.foundation.layout.Spacer(modifier = Modifier.weight(1f)) + trailing.invoke() + } + } +} + +private fun Modifier.weight(@Suppress("UNUSED_PARAMETER") f: Float): Modifier = this diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt new file mode 100644 index 000000000..d9bf02b81 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt @@ -0,0 +1,56 @@ +package zed.rainxch.core.presentation.components.overlays + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.components.buttons.IconButton + +/** + * Full-screen sheet for multi-step flows (DESIGN.md §16.5) — OAuth device-flow, + * PAT entry, ExternalImport wizard. Back-arrow header, no title text (the big + * identity mark serves as the title). Caller composes body via slot. + */ +@Composable +fun GhsFullScreenSheet( + onBack: () -> Unit, + modifier: Modifier = Modifier, + backContentDescription: String = "Back", + content: @Composable () -> Unit, +) { + val cs = MaterialTheme.colorScheme + Column( + modifier = modifier + .fillMaxSize() + .background(cs.background), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = backContentDescription, + tint = cs.onSurface, + ) + } + } + Box(modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp)) { + content() + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt new file mode 100644 index 000000000..5e8fbc0cb --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt @@ -0,0 +1,56 @@ +package zed.rainxch.core.presentation.components.overlays + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape + +/** Toast tint variants (DESIGN.md §16.3). */ +enum class ToastTint { Default, Success, Error, Info } + +/** + * Wonky-squircle toast (DESIGN.md §16.3). Pure visual layer — caller wires + * `SnackbarHost` / `SnackbarHostState` for lifecycle. Use [Default] for surface + * neutral, [Success] / [Error] for tinted, [Info] for primary tint. + */ +@Composable +fun GhsToast( + modifier: Modifier = Modifier, + tint: ToastTint = ToastTint.Default, + leading: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + content: @Composable () -> Unit, +) { + val cs = MaterialTheme.colorScheme + val (bg, border) = when (tint) { + ToastTint.Default -> cs.surface to cs.outline + ToastTint.Success -> cs.tertiaryContainer to cs.tertiary.copy(alpha = 0.55f) + ToastTint.Error -> cs.errorContainer to cs.error.copy(alpha = 0.55f) + ToastTint.Info -> cs.primaryContainer to cs.primary.copy(alpha = 0.55f) + } + Row( + modifier = modifier + .clip(WonkySquircleShape.Toast) + .background(bg) + .border(width = 1.dp, color = border, shape = WonkySquircleShape.Toast) + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + leading?.invoke() + ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { + content() + } + trailing?.invoke() + } +} From a63004d4b485a3970bd5a0cfd20e13d127a8fdbe Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:33:06 +0500 Subject: [PATCH 013/172] feat(chrome): Cookie BottomNav, Desktop drawer, Library label --- .../app/navigation/AppNavigation.kt | 882 +++++++++--------- .../app/navigation/BottomNavigation.kt | 419 ++------- .../app/navigation/BottomNavigationUtils.kt | 10 +- .../app/navigation/DesktopDrawer.kt | 178 ++++ .../composeResources/values-ar/strings-ar.xml | 2 +- .../composeResources/values-bn/strings-bn.xml | 2 +- .../composeResources/values-es/strings-es.xml | 2 +- .../composeResources/values-fr/strings-fr.xml | 2 +- .../composeResources/values-hi/strings-hi.xml | 2 +- .../composeResources/values-it/strings-it.xml | 2 +- .../composeResources/values-ja/strings-ja.xml | 2 +- .../composeResources/values-ko/strings-ko.xml | 2 +- .../composeResources/values-pl/strings-pl.xml | 2 +- .../composeResources/values-ru/strings-ru.xml | 2 +- .../composeResources/values-tr/strings-tr.xml | 2 +- .../values-zh-rCN/strings-zh-rCN.xml | 2 +- .../composeResources/values/strings.xml | 2 +- 17 files changed, 746 insertions(+), 769 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 8363ba8ba..6fb5e5a6e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -2,6 +2,8 @@ package zed.rainxch.githubstore.app.navigation import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -30,7 +32,9 @@ import zed.rainxch.apps.presentation.AppsRoot import zed.rainxch.apps.presentation.AppsViewModel import zed.rainxch.apps.presentation.import.ExternalImportRoot import zed.rainxch.auth.presentation.AuthenticationRoot +import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.ContentWidth +import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.components.announcements.AnnouncementsRoot import zed.rainxch.core.presentation.components.whatsnew.WhatsNewHistoryScreen import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight @@ -75,471 +79,493 @@ fun AppNavigation( val announcementsViewModel = koinViewModel() val announcementsUnreadCount by announcementsViewModel.unreadCount.collectAsStateWithLifecycle() + val isDesktop = getPlatform() != Platform.ANDROID + CompositionLocalProvider( LocalBottomNavigationHeight provides bottomNavigationHeight, LocalScrollbarEnabled provides isScrollbarEnabled, LocalContentWidth provides contentWidth, ) { - Box( - modifier = Modifier.fillMaxSize(), - ) { - NavHost( - navController = navController, - startDestination = GithubStoreGraph.HomeScreen, - modifier = Modifier.background(MaterialTheme.colorScheme.background), + Row(modifier = Modifier.fillMaxSize()) { + val desktopDrawerCurrent = + navController.currentBackStackEntryAsState().value.getCurrentScreen() + if (isDesktop && desktopDrawerCurrent != null) { + DesktopDrawer( + currentScreen = desktopDrawerCurrent, + onNavigate = { target -> + navController.navigate(target) { + popUpTo(GithubStoreGraph.HomeScreen) { saveState = true } + launchSingleTop = true + restoreState = true + } + }, + isUpdateAvailable = + appsState.apps.any { it.installedApp.isUpdateAvailable } || + appsState.showImportProposalBanner, + hasUnreadAnnouncements = announcementsUnreadCount > 0, + ) + } + Box( + modifier = if (isDesktop) Modifier.weight(1f).fillMaxHeight() else Modifier.fillMaxSize(), ) { - composable { - HomeRoot( - onNavigateToSearch = { - navController.navigate(GithubStoreGraph.SearchScreen()) - }, - onNavigateToSettings = { - navController.navigate(GithubStoreGraph.ProfileScreen) - }, - onNavigateToApps = { - navController.navigate(GithubStoreGraph.AppsScreen) - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + NavHost( + navController = navController, + startDestination = GithubStoreGraph.HomeScreen, + modifier = Modifier.background(MaterialTheme.colorScheme.background), + ) { + composable { + HomeRoot( + onNavigateToSearch = { + navController.navigate(GithubStoreGraph.SearchScreen()) + }, + onNavigateToSettings = { + navController.navigate(GithubStoreGraph.ProfileScreen) + }, + onNavigateToApps = { + navController.navigate(GithubStoreGraph.AppsScreen) + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - val initialPlatform = - args.initialPlatform?.let { name -> - runCatching { - zed.rainxch.search.presentation.model.SearchPlatformUi - .valueOf(name) - }.getOrNull() - } - SearchRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId, sourceHost -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - sourceHost = sourceHost, - ), - ) - }, - onNavigateToDetailsFromLink = { owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - owner = owner, - repo = repo, - ), - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - viewModel = - koinViewModel { - parametersOf(initialPlatform) + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val initialPlatform = + args.initialPlatform?.let { name -> + runCatching { + zed.rainxch.search.presentation.model.SearchPlatformUi + .valueOf(name) + }.getOrNull() + } + SearchRoot( + onNavigateBack = { + navController.navigateUp() }, - ) - } + onNavigateToDetails = { repoId, sourceHost -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + sourceHost = sourceHost, + ), + ) + }, + onNavigateToDetailsFromLink = { owner, repo -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + owner = owner, + repo = repo, + ), + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + viewModel = + koinViewModel { + parametersOf(initialPlatform) + }, + ) + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - DetailsRoot( - onNavigateBack = { - navController.navigateUp() - }, - onOpenRepositoryInApp = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - onNavigateToSearchByPlatform = { platform -> - navController.navigate( - GithubStoreGraph.SearchScreen( - initialPlatform = platform.toSearchPlatformUi().name, - ), - ) - }, - viewModel = - koinViewModel { - parametersOf( - args.repositoryId, - args.owner, - args.repo, - args.isComingFromUpdate, - args.sourceHost, + composable { backStackEntry -> + val args = backStackEntry.toRoute() + DetailsRoot( + onNavigateBack = { + navController.navigateUp() + }, + onOpenRepositoryInApp = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), ) }, - ) - } + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + onNavigateToSearchByPlatform = { platform -> + navController.navigate( + GithubStoreGraph.SearchScreen( + initialPlatform = platform.toSearchPlatformUi().name, + ), + ) + }, + viewModel = + koinViewModel { + parametersOf( + args.repositoryId, + args.owner, + args.repo, + args.isComingFromUpdate, + args.sourceHost, + ) + }, + ) + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - DeveloperProfileRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - viewModel = - koinViewModel { - parametersOf(args.username) + composable { backStackEntry -> + val args = backStackEntry.toRoute() + DeveloperProfileRoot( + onNavigateBack = { + navController.navigateUp() }, - ) - } + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + viewModel = + koinViewModel { + parametersOf(args.username) + }, + ) + } - composable { - AuthenticationRoot( - onNavigateToHome = { - navController.navigate(GithubStoreGraph.HomeScreen) { - popUpTo(0) { - inclusive = true + composable { + AuthenticationRoot( + onNavigateToHome = { + navController.navigate(GithubStoreGraph.HomeScreen) { + popUpTo(0) { + inclusive = true + } } - } - }, - ) - } + }, + ) + } - composable { - FavouritesRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { - navController.navigate(GithubStoreGraph.DetailsScreen(it)) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + FavouritesRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { + navController.navigate(GithubStoreGraph.DetailsScreen(it)) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - StarredReposRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToAuthentication = { - navController.navigate( - GithubStoreGraph.AuthenticationScreen, - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + StarredReposRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + onNavigateToAuthentication = { + navController.navigate( + GithubStoreGraph.AuthenticationScreen, + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - zed.rainxch.apps.presentation.starred.StarredPickerRoot( - onNavigateBack = { navController.navigateUp() }, - onNavigateToDetails = { repoId, owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - owner = owner, - repo = repo, - ), - ) - }, - ) - } + composable { + zed.rainxch.apps.presentation.starred.StarredPickerRoot( + onNavigateBack = { navController.navigateUp() }, + onNavigateToDetails = { repoId, owner, repo -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + ), + ) + }, + ) + } - composable { - ProfileRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToAuthentication = { - navController.navigate(GithubStoreGraph.AuthenticationScreen) - }, - onNavigateToStarredRepos = { - navController.navigate(GithubStoreGraph.StarredReposScreen) - }, - onNavigateToFavouriteRepos = { - navController.navigate(GithubStoreGraph.FavouritesScreen) - }, - onNavigateToRecentlyViewed = { - navController.navigate(GithubStoreGraph.RecentlyViewedScreen) - }, - onNavigateToDevProfile = { username -> - navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username)) - }, - onNavigateToWhatsNew = { - navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) - }, - onPreviewWhatsNewSheet = { - whatsNewViewModel.forceShowLatest() - navController.navigateUp() - }, - onNavigateToAnnouncements = { - navController.navigate(GithubStoreGraph.AnnouncementsScreen) - }, - onPreviewAnnouncements = { - announcementsViewModel.previewSampleAnnouncements() - navController.navigate(GithubStoreGraph.AnnouncementsScreen) - }, - hasUnreadAnnouncements = announcementsUnreadCount > 0, - ) - } + composable { + ProfileRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToAuthentication = { + navController.navigate(GithubStoreGraph.AuthenticationScreen) + }, + onNavigateToStarredRepos = { + navController.navigate(GithubStoreGraph.StarredReposScreen) + }, + onNavigateToFavouriteRepos = { + navController.navigate(GithubStoreGraph.FavouritesScreen) + }, + onNavigateToRecentlyViewed = { + navController.navigate(GithubStoreGraph.RecentlyViewedScreen) + }, + onNavigateToDevProfile = { username -> + navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username)) + }, + onNavigateToWhatsNew = { + navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) + }, + onPreviewWhatsNewSheet = { + whatsNewViewModel.forceShowLatest() + navController.navigateUp() + }, + onNavigateToAnnouncements = { + navController.navigate(GithubStoreGraph.AnnouncementsScreen) + }, + onPreviewAnnouncements = { + announcementsViewModel.previewSampleAnnouncements() + navController.navigate(GithubStoreGraph.AnnouncementsScreen) + }, + hasUnreadAnnouncements = announcementsUnreadCount > 0, + ) + } - composable { - RecentlyViewedRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + RecentlyViewedRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - MirrorPickerRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + MirrorPickerRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - val historyEntries by whatsNewViewModel.historyEntries.collectAsStateWithLifecycle() - WhatsNewHistoryScreen( - entries = historyEntries, - onNavigateBack = { navController.navigateUp() }, - ) - } + composable { + val historyEntries by whatsNewViewModel.historyEntries.collectAsStateWithLifecycle() + WhatsNewHistoryScreen( + entries = historyEntries, + onNavigateBack = { navController.navigateUp() }, + ) + } - composable { - val feed by announcementsViewModel.feed.collectAsStateWithLifecycle() - val displayed by announcementsViewModel.displayedItems.collectAsStateWithLifecycle() - AnnouncementsRoot( - items = displayed, - acknowledgedIds = feed.acknowledgedIds, - mutedCategories = feed.mutedCategories, - refreshFailed = feed.lastRefreshFailed, - onNavigateBack = { navController.navigateUp() }, - onRefresh = { announcementsViewModel.refresh() }, - onCtaClick = { announcementsViewModel.openCta(it) }, - onDismissClick = { announcementsViewModel.dismiss(it) }, - onAcknowledgeClick = { announcementsViewModel.acknowledge(it) }, - onToggleMute = { category, muted -> - announcementsViewModel.setMuted(category, muted) - }, - onLeavingScreen = { announcementsViewModel.clearPreview() }, - onEnteringScreen = { announcementsViewModel.markRoutineItemsSeen() }, - ) - } + composable { + val feed by announcementsViewModel.feed.collectAsStateWithLifecycle() + val displayed by announcementsViewModel.displayedItems.collectAsStateWithLifecycle() + AnnouncementsRoot( + items = displayed, + acknowledgedIds = feed.acknowledgedIds, + mutedCategories = feed.mutedCategories, + refreshFailed = feed.lastRefreshFailed, + onNavigateBack = { navController.navigateUp() }, + onRefresh = { announcementsViewModel.refresh() }, + onCtaClick = { announcementsViewModel.openCta(it) }, + onDismissClick = { announcementsViewModel.dismiss(it) }, + onAcknowledgeClick = { announcementsViewModel.acknowledge(it) }, + onToggleMute = { category, muted -> + announcementsViewModel.setMuted(category, muted) + }, + onLeavingScreen = { announcementsViewModel.clearPreview() }, + onEnteringScreen = { announcementsViewModel.markRoutineItemsSeen() }, + ) + } - composable { - TweaksRoot( - onNavigateToMirrorPicker = { - navController.navigate(GithubStoreGraph.MirrorPickerScreen) { - launchSingleTop = true - } - }, - onNavigateToSkippedUpdates = { - navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { - launchSingleTop = true - } - }, - onNavigateToHiddenRepositories = { - navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { - launchSingleTop = true - } - }, - onNavigateToHostTokens = { - navController.navigate(GithubStoreGraph.HostTokensScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksRoot( + onNavigateToMirrorPicker = { + navController.navigate(GithubStoreGraph.MirrorPickerScreen) { + launchSingleTop = true + } + }, + onNavigateToSkippedUpdates = { + navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { + launchSingleTop = true + } + }, + onNavigateToHiddenRepositories = { + navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + launchSingleTop = true + } + }, + onNavigateToHostTokens = { + navController.navigate(GithubStoreGraph.HostTokensScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - SkippedUpdatesRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + SkippedUpdatesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - HiddenRepositoriesRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + HiddenRepositoriesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - HostTokensRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + HostTokensRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { backStackEntry -> - // Pick up the "open link sheet" flag set by ExternalImportRoot's - // "Add manually" path. We consume the flag once on entry so a - // later config change or back-stack rewind doesn't reopen the sheet. - LaunchedEffect(backStackEntry) { - val handle = backStackEntry.savedStateHandle - val openLinkSheet = handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) - if (openLinkSheet == true) { - handle.remove(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) - appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) + composable { backStackEntry -> + // Pick up the "open link sheet" flag set by ExternalImportRoot's + // "Add manually" path. We consume the flag once on entry so a + // later config change or back-stack rewind doesn't reopen the sheet. + LaunchedEffect(backStackEntry) { + val handle = backStackEntry.savedStateHandle + val openLinkSheet = handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) + if (openLinkSheet == true) { + handle.remove(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) + appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) + } } + AppsRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToRepo = { repoId, sourceHost, owner, repo -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + isComingFromUpdate = true, + sourceHost = sourceHost, + owner = owner.orEmpty(), + repo = repo.orEmpty(), + ), + ) + }, + onNavigateToExternalImport = { + navController.navigate(GithubStoreGraph.ExternalImportScreen) + }, + onNavigateToStarredPicker = { + navController.navigate(GithubStoreGraph.StarredPickerScreen) + }, + viewModel = appsViewModel, + state = appsState, + ) + } + + composable { + ExternalImportRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + isComingFromUpdate = true, + ), + ) + }, + onAddManually = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY, true) + navController.navigateUp() + }, + ) } - AppsRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToRepo = { repoId, sourceHost, owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - isComingFromUpdate = true, - sourceHost = sourceHost, - owner = owner.orEmpty(), - repo = repo.orEmpty(), - ), - ) - }, - onNavigateToExternalImport = { - navController.navigate(GithubStoreGraph.ExternalImportScreen) - }, - onNavigateToStarredPicker = { - navController.navigate(GithubStoreGraph.StarredPickerScreen) - }, - viewModel = appsViewModel, - state = appsState, - ) } - composable { - ExternalImportRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - isComingFromUpdate = true, - ), - ) - }, - onAddManually = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY, true) - navController.navigateUp() + val currentScreen = + navController.currentBackStackEntryAsState().value.getCurrentScreen() + + currentScreen?.takeIf { !isDesktop }?.let { + BottomNavigation( + currentScreen = currentScreen, + onNavigate = { + navController.navigate(it) { + popUpTo(GithubStoreGraph.HomeScreen) { + saveState = true + } + + launchSingleTop = true + restoreState = true + } }, + // Badge fires when either an update is waiting OR pending + // import candidates need review. The badge is a single dot + // — a union of the two conditions is honest "you have + // something to look at on this tab". + isUpdateAvailable = + appsState.apps.any { it.installedApp.isUpdateAvailable } || + appsState.showImportProposalBanner, + hasUnreadAnnouncements = announcementsUnreadCount > 0, + modifier = + Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(bottom = 24.dp) + .onGloballyPositioned { coordinates -> + bottomNavigationHeight = + with(density) { coordinates.size.height.toDp() } + }, ) } - } - val currentScreen = - navController.currentBackStackEntryAsState().value.getCurrentScreen() - - currentScreen?.let { - BottomNavigation( - currentScreen = currentScreen, - onNavigate = { - navController.navigate(it) { - popUpTo(GithubStoreGraph.HomeScreen) { - saveState = true + val autoSuggestVm: AutoSuggestMirrorViewModel = koinViewModel() + val isAutoSuggestVisible by autoSuggestVm.isVisible.collectAsStateWithLifecycle() + if (isAutoSuggestVisible) { + AutoSuggestMirrorSheet( + onDismiss = autoSuggestVm::dismiss, + onPickOne = { + autoSuggestVm.onPickOneClicked() + navController.navigate(GithubStoreGraph.MirrorPickerScreen) { + launchSingleTop = true } - - launchSingleTop = true - restoreState = true - } - }, - // Badge fires when either an update is waiting OR pending - // import candidates need review. The badge is a single dot - // — a union of the two conditions is honest "you have - // something to look at on this tab". - isUpdateAvailable = - appsState.apps.any { it.installedApp.isUpdateAvailable } || - appsState.showImportProposalBanner, - hasUnreadAnnouncements = announcementsUnreadCount > 0, - modifier = - Modifier - .align(Alignment.BottomCenter) - .navigationBarsPadding() - .padding(bottom = 24.dp) - .onGloballyPositioned { coordinates -> - bottomNavigationHeight = - with(density) { coordinates.size.height.toDp() } - }, - ) - } - - val autoSuggestVm: AutoSuggestMirrorViewModel = koinViewModel() - val isAutoSuggestVisible by autoSuggestVm.isVisible.collectAsStateWithLifecycle() - if (isAutoSuggestVisible) { - AutoSuggestMirrorSheet( - onDismiss = autoSuggestVm::dismiss, - onPickOne = { - autoSuggestVm.onPickOneClicked() - navController.navigate(GithubStoreGraph.MirrorPickerScreen) { - launchSingleTop = true - } - }, - onMaybeLater = autoSuggestVm::onMaybeLater, - onDontAskAgain = autoSuggestVm::onDontAskAgain, - ) + }, + onMaybeLater = autoSuggestVm::onMaybeLater, + onDontAskAgain = autoSuggestVm::onDontAskAgain, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt index c34b29fae..a12ffeb75 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt @@ -1,22 +1,21 @@ package zed.rainxch.githubstore.app.navigation -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -24,38 +23,34 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.domain.getPlatform -import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.theme.GithubStoreTheme - +import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.vocabulary.CookieShape +import zed.rainxch.core.presentation.vocabulary.VersionStack + +/** + * Cookie-active bottom nav (DESIGN.md §9.1). Active tab fills a [CookieShape] with + * `primary`, knocks the glyph out in `onPrimary`, and renders the label in Fraunces + * italic. Library tab shows a [VersionStack] badge when updates are pending. + * + * Heavy press-scale + spring physics matches D10 "rich motion." Per + * android-compose-ui skill: animated values drive `graphicsLayer` / `scale` to + * avoid recomposition. + */ @Composable fun BottomNavigation( currentScreen: GithubStoreGraph, @@ -67,57 +62,8 @@ fun BottomNavigation( val allowedScreens = BottomNavigationUtils.allowedScreens() if (allowedScreens.none { it.screen::class == currentScreen::class }) return - val selectedIndex = - allowedScreens.indexOfFirst { it.screen::class == currentScreen::class } - - val itemPositions = remember { mutableMapOf>() } - - var selectedItemPos by remember { mutableStateOf?>(null) } - - val rowHorizontalPaddingDp = 6.dp - val density = LocalDensity.current - val rowHorizontalPaddingPx = with(density) { rowHorizontalPaddingDp.toPx() } - - val indicatorHorizontalInsetPx = with(density) { 4.dp.toPx() } - - val indicatorX = remember { Animatable(0f) } - val indicatorWidth = remember { Animatable(0f) } - - LaunchedEffect(selectedIndex, selectedItemPos) { - val raw = selectedItemPos ?: itemPositions[selectedIndex] ?: return@LaunchedEffect - val targetX = raw.first + rowHorizontalPaddingPx - indicatorHorizontalInsetPx - val targetW = raw.second + indicatorHorizontalInsetPx * 2f - launch { - // E4.2: tab indicator animated via tween — bouncy spring on a - // high-frequency tap target overshoots on every tab change and - // reads as jittery (survey #16). Tween stays predictable. - indicatorX.animateTo( - targetValue = targetX, - animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), - ) - } - launch { - indicatorWidth.animateTo( - targetValue = targetW, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ) - } - } - - val isDarkTheme = - !MaterialTheme.colorScheme.background - .luminance() - .let { it > 0.5f } - - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - Box( + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Row( modifier = Modifier .clip(CircleShape) @@ -126,222 +72,50 @@ fun BottomNavigation( width = 1.dp, color = MaterialTheme.colorScheme.outlineVariant, shape = CircleShape, - ).pointerInput(Unit) { }, + ).padding(horizontal = 8.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, ) { - val glassHighColor = - if (isDarkTheme) { - Color.White.copy(alpha = .12f) - } else { - Color.White.copy(alpha = .30f) - } - val glassLowColor = - if (isDarkTheme) { - Color.White.copy(alpha = .04f) - } else { - Color.White.copy(alpha = .10f) - } - val specularColor = - if (isDarkTheme) { - Color.White.copy(alpha = .18f) - } else { - Color.White.copy(alpha = .45f) - } - val innerGlowColor = - if (isDarkTheme) { - Color.White.copy(alpha = .03f) - } else { - Color.White.copy(alpha = .08f) - } - val borderColor = - if (isDarkTheme) { - Color.White.copy(alpha = .08f) - } else { - Color.Transparent - } - - Box( - modifier = - Modifier - .matchParentSize() - .drawBehind { - if (indicatorWidth.value > 0f) { - if (isDarkTheme) { - drawRoundRect( - color = borderColor, - topLeft = - Offset( - indicatorX.value - .5.dp.toPx(), - 1.5.dp.toPx(), - ), - size = - Size( - indicatorWidth.value + 1.dp.toPx(), - size.height - 3.dp.toPx(), - ), - cornerRadius = CornerRadius(size.height / 2f), - style = Stroke(width = 1.dp.toPx()), - ) - } - - drawRoundRect( - brush = - Brush.verticalGradient( - colors = listOf(glassHighColor, glassLowColor), - ), - topLeft = Offset(indicatorX.value, 2.dp.toPx()), - size = Size(indicatorWidth.value, size.height - 4.dp.toPx()), - cornerRadius = CornerRadius(size.height / 2f), - ) - - drawRoundRect( - brush = - Brush.horizontalGradient( - colors = - listOf( - Color.Transparent, - specularColor, - Color.Transparent, - ), - startX = indicatorX.value + indicatorWidth.value * .15f, - endX = indicatorX.value + indicatorWidth.value * .85f, - ), - topLeft = - Offset( - indicatorX.value + indicatorWidth.value * .15f, - 3.dp.toPx(), - ), - size = Size(indicatorWidth.value * .7f, 1.5.dp.toPx()), - cornerRadius = CornerRadius(1.dp.toPx()), - ) - - drawRoundRect( - brush = - Brush.verticalGradient( - colors = listOf(Color.Transparent, innerGlowColor), - ), - topLeft = - Offset( - indicatorX.value + 4.dp.toPx(), - size.height - 8.dp.toPx(), - ), - size = Size(indicatorWidth.value - 8.dp.toPx(), 4.dp.toPx()), - cornerRadius = CornerRadius(2.dp.toPx()), - ) - } - }, - ) - - Row( - modifier = Modifier.padding(horizontal = rowHorizontalPaddingDp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - BottomNavigationUtils.allowedScreens().forEachIndexed { index, item -> - LiquidGlassTabItem( - item = item, - hasBadge = - (item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable) || - (item.screen == GithubStoreGraph.ProfileScreen && hasUnreadAnnouncements), - isSelected = item.screen::class == currentScreen::class, - onSelect = { onNavigate(item.screen) }, - onPositioned = { x, width -> - itemPositions[index] = x to width - if (index == selectedIndex) { - selectedItemPos = x to width - } - if (index == selectedIndex && indicatorWidth.value == 0f) { - val snapX = x + rowHorizontalPaddingPx - indicatorHorizontalInsetPx - val snapW = width + indicatorHorizontalInsetPx * 2f - indicatorX.snapTo(snapX) - indicatorWidth.snapTo(snapW) - } - }, - ) - } + allowedScreens.forEach { item -> + CookieTabItem( + item = item, + isSelected = item.screen::class == currentScreen::class, + onSelect = { onNavigate(item.screen) }, + showUpdateBadge = item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable, + hasUnreadDot = item.screen == GithubStoreGraph.ProfileScreen && hasUnreadAnnouncements, + ) } } } } @Composable -private fun LiquidGlassTabItem( +private fun CookieTabItem( item: BottomNavigationItem, isSelected: Boolean, onSelect: () -> Unit, - hasBadge: Boolean = false, - onPositioned: suspend (x: Float, width: Float) -> Unit, + showUpdateBadge: Boolean = false, + hasUnreadDot: Boolean = false, ) { - val scope = rememberCoroutineScope() - val density = LocalDensity.current val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - val pressScale by animateFloatAsState( - targetValue = if (isPressed) 0.85f else 1f, + targetValue = 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium, ), - label = "pressScale", - ) - - val iconScale by animateFloatAsState( - targetValue = if (isSelected) 1.15f else 1f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - label = "iconScale", - ) - - val iconOffsetY by animateDpAsState( - targetValue = if (isSelected) (-1).dp else 1.dp, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - label = "iconOffsetY", + label = "tab-press-scale", ) - - val iconTint = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = .7f) - } - - val labelAlpha by animateFloatAsState( + val activeAlpha by animateFloatAsState( targetValue = if (isSelected) 1f else 0f, - animationSpec = - tween( - durationMillis = if (isSelected) 250 else 150, - easing = FastOutSlowInEasing, - ), - label = "labelAlpha", - ) - - val labelScale by animateFloatAsState( - targetValue = if (isSelected) 1f else 0.6f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - label = "labelScale", - ) - - val horizontalPadding by animateDpAsState( - targetValue = if (isSelected) 14.dp else 10.dp, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow, - ), - label = "hPadding", + animationSpec = spring(stiffness = Spring.StiffnessMedium), + label = "tab-active-alpha", ) + val cs = MaterialTheme.colorScheme + val cookieFill = cs.primary + val activeFg = cs.onPrimary + val inactiveFg = cs.onSurface.copy(alpha = 0.7f) Box( modifier = @@ -350,71 +124,77 @@ private fun LiquidGlassTabItem( .clickable( interactionSource = interactionSource, indication = null, - ) { onSelect() } - .onGloballyPositioned { coordinates -> - val x = coordinates.positionInParent().x - val width = coordinates.size.width.toFloat() - scope.launch { onPositioned(x, width) } - }.graphicsLayer { - scaleX = pressScale - scaleY = pressScale - }.padding(horizontal = horizontalPadding, vertical = 6.dp), + onClick = onSelect, + ).scale(pressScale) + .padding(horizontal = if (isSelected) 14.dp else 10.dp, vertical = 6.dp), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(1.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Icon( - imageVector = if (isSelected) item.iconFilled else item.iconOutlined, - contentDescription = stringResource(item.titleRes), - modifier = - Modifier - .size(22.dp) - .graphicsLayer { - scaleX = iconScale - scaleY = iconScale - translationY = with(density) { iconOffsetY.toPx() } - }, - tint = iconTint, - ) - + // Cookie + glyph stack Box( - modifier = - Modifier - .height(if (isSelected) 16.dp else 0.dp) - .graphicsLayer { - alpha = labelAlpha - scaleX = labelScale - scaleY = labelScale - }, + modifier = Modifier.size(32.dp), contentAlignment = Alignment.Center, + ) { + if (activeAlpha > 0f) { + Box( + modifier = + Modifier + .size(32.dp) + .graphicsLayer { alpha = activeAlpha } + .clip(CookieShape) + .background(cookieFill), + ) + } + Icon( + imageVector = if (isSelected) item.iconFilled else item.iconOutlined, + contentDescription = stringResource(item.titleRes), + modifier = Modifier.size(20.dp), + tint = if (isSelected) activeFg else inactiveFg, + ) + } + // Active label — Fraunces italic + AnimatedVisibility( + visible = isSelected, + enter = fadeIn() + scaleIn(initialScale = 0.6f), + exit = fadeOut() + scaleOut(targetScale = 0.6f), ) { Text( text = stringResource(item.titleRes), + color = cs.primary, style = MaterialTheme.typography.labelSmall.copy( - fontSize = 10.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - lineHeight = 12.sp, + fontFamily = fraunces, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, ), - color = - if (isSelected) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = .7f) - }, maxLines = 1, ) } } - if (hasBadge) { + // Update badge (Library tab) — VersionStack replaces M3 numeric badge + if (showUpdateBadge) { Box( - Modifier - .size(12.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.error) - .align(Alignment.TopEnd), + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(top = 2.dp), + ) { + VersionStack(count = 1, widthDp = 8) + } + } + if (hasUnreadDot) { + Box( + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(top = 4.dp, end = 2.dp) + .size(8.dp) + .clip(CircleShape) + .background(cs.error), ) } } @@ -426,8 +206,7 @@ fun BottomNavigationPreview() { GithubStoreTheme { BottomNavigation( currentScreen = GithubStoreGraph.HomeScreen, - onNavigate = { - }, + onNavigate = {}, isUpdateAvailable = true, ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt index c5c1aecae..d3614a912 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt @@ -5,8 +5,6 @@ import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.ui.graphics.vector.ImageVector import org.jetbrains.compose.resources.StringResource -import zed.rainxch.core.domain.getPlatform -import zed.rainxch.core.domain.model.Platform import zed.rainxch.githubstore.core.presentation.res.* data class BottomNavigationItem( @@ -45,10 +43,6 @@ object BottomNavigationUtils { ), ) - fun allowedScreens(): List = - items() - .filterNot { - getPlatform() != Platform.ANDROID && - it.screen == GithubStoreGraph.AppsScreen - } + /** Bottom-nav (Android) shows the same items as the Desktop drawer. */ + fun allowedScreens(): List = items() } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt new file mode 100644 index 000000000..fed4b5bb1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt @@ -0,0 +1,178 @@ +package zed.rainxch.githubstore.app.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.CookieShape +import zed.rainxch.core.presentation.vocabulary.VersionStack +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.app_name + +/** + * Desktop sidebar drawer (DESIGN.md §8.1) — 240dp wide, persistent navigation. + * Cookie brand mark at top, nav items in middle, user card at bottom. Active item: + * `tintP` background + `primary` foreground. + * + * Wired by [AppNavigation] when the platform is non-Android. The Android path + * keeps the [BottomNavigation] capsule. + */ +@Composable +fun DesktopDrawer( + currentScreen: GithubStoreGraph?, + onNavigate: (GithubStoreGraph) -> Unit, + isUpdateAvailable: Boolean, + hasUnreadAnnouncements: Boolean, + modifier: Modifier = Modifier, +) { + val cs = MaterialTheme.colorScheme + val items = BottomNavigationUtils.items() + Column( + modifier = + modifier + .fillMaxHeight() + .width(240.dp) + .background(cs.surface) + .padding(vertical = 16.dp), + verticalArrangement = Arrangement.Top, + ) { + // Brand: Cookie + name + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = + Modifier + .size(28.dp) + .clip(CookieShape) + .background(cs.primary), + contentAlignment = Alignment.Center, + ) { + Text( + text = "G", + color = Color.White, + style = + MaterialTheme.typography.titleMedium.copy( + fontFamily = fraunces, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + fontWeight = FontWeight.Bold, + fontSize = 15.sp, + ), + ) + } + Text( + text = stringResource(Res.string.app_name), + color = cs.onSurface, + style = + MaterialTheme.typography.titleMedium.copy( + fontFamily = fraunces, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + ) + } + Spacer(Modifier.size(16.dp)) + items.forEach { item -> + val isSelected = item.screen::class == currentScreen?.let { it::class } + val badge: (@Composable () -> Unit)? = + when { + item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable -> { + { VersionStack(count = 1, widthDp = 8) } + } + + item.screen == GithubStoreGraph.ProfileScreen && hasUnreadAnnouncements -> { + { UnreadDot(cs.error) } + } + + else -> { + null + } + } + DrawerNavItem( + label = stringResource(item.titleRes), + iconImage = if (isSelected) item.iconFilled else item.iconOutlined, + isSelected = isSelected, + onClick = { onNavigate(item.screen) }, + trailing = badge, + ) + } + } +} + +@Composable +private fun DrawerNavItem( + label: String, + iconImage: androidx.compose.ui.graphics.vector.ImageVector, + isSelected: Boolean, + onClick: () -> Unit, + trailing: (@Composable () -> Unit)? = null, +) { + val cs = MaterialTheme.colorScheme + val bg = if (isSelected) cs.primaryContainer else Color.Transparent + val fg = if (isSelected) cs.primary else cs.onSurface + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 2.dp) + .clip(Radii.row) + .background(bg) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = iconImage, + contentDescription = label, + modifier = Modifier.size(20.dp), + tint = fg, + ) + Text( + text = label, + color = fg, + style = + MaterialTheme.typography.bodyMedium.copy( + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + ), + modifier = Modifier.weight(1f), + ) + trailing?.invoke() + } +} + +@Composable +private fun UnreadDot(color: Color) { + Box( + modifier = + Modifier + .size(8.dp) + .clip(CircleShape) + .background(color), + ) +} diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 96ff5dbed..b49bf756c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -420,7 +420,7 @@ الرئيسية البحث - التطبيقات + المكتبة الملف الشخصي نسخة متفرعة diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index fc4e54cf5..9fce95c6b 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -371,7 +371,7 @@ হোম অনুসন্ধান - অ্যাপস + লাইব্রেরি প্রোফাইল ফর্ক diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 5a43d6396..6271addb5 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -317,7 +317,7 @@ Inicio Buscar - Aplicaciones + Biblioteca Perfil Bifurcar diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 7628d8d57..1910b3fa1 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -318,7 +318,7 @@ Accueil Rechercher - Applications + Bibliothèque Profil Fork diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index e1f15d174..69c14f331 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -359,7 +359,7 @@ होम खोज - ऐप्स + लाइब्रेरी प्रोफ़ाइल फोर्क diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 26b5aadb7..92ddf6513 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -367,7 +367,7 @@ Home Cerca - App + Libreria Profilo Fork diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index ef0f1f8e1..5f3af264e 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -318,7 +318,7 @@ ホーム 検索 - アプリ + ライブラリ プロフィール フォーク diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 579407cfe..003cae28a 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -369,7 +369,7 @@ 검색 - + 라이브러리 프로필 포크 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 953ac5b99..9ab37f9a3 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -334,7 +334,7 @@ Strona główna Szukaj - Aplikacje + Biblioteka Profil Fork diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 3969fbfd9..964d62b6f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -336,7 +336,7 @@ Главная Поиск - Приложения + Библиотека Профиль Форк diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 1426e5b7b..38c6889ca 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -368,7 +368,7 @@ Ana Sayfa Ara - Uygulamalar + Kütüphane Profil Çatalla diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index cd8fb2263..f8a647e26 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -319,7 +319,7 @@ 首页 搜索 - 应用 + 应用库 个人资料 分叉 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index def041cf5..55ed1e3b0 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -479,7 +479,7 @@ Home Search - Apps + Library Profile Tweaks From 2b17908ae4217beb97ccd00e0b683a4020f50b89 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:40:33 +0500 Subject: [PATCH 014/172] feat(onboarding): 3-step first launch + drawer Library fix --- .../kotlin/zed/rainxch/githubstore/Main.kt | 13 + .../zed/rainxch/githubstore/MainState.kt | 2 + .../zed/rainxch/githubstore/MainViewModel.kt | 8 + .../githubstore/app/di/ViewModelsModule.kt | 2 + .../app/navigation/AppNavigation.kt | 13 + .../app/navigation/DesktopDrawer.kt | 5 +- .../app/onboarding/OnboardingAction.kt | 24 ++ .../app/onboarding/OnboardingEvent.kt | 7 + .../app/onboarding/OnboardingScreen.kt | 344 ++++++++++++++++++ .../app/onboarding/OnboardingState.kt | 21 ++ .../app/onboarding/OnboardingViewModel.kt | 88 +++++ .../data/repository/TweaksRepositoryImpl.kt | 8 + .../domain/repository/TweaksRepository.kt | 5 + 13 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingAction.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingEvent.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt index 029a5b697..410e9386d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt @@ -49,6 +49,19 @@ fun App(deepLinkUri: String? = null) { val navController = rememberNavController() val currentScreen = navController.currentBackStackEntryAsState().value.getCurrentScreen() + // First-launch redirect to Onboarding (D17 / D6.5). Fires once when the + // persistence layer reports `onboardingComplete = false`. `null` means the + // flow hasn't emitted yet — wait, don't redirect into a flash of Home. + LaunchedEffect(state.onboardingComplete) { + if (state.onboardingComplete == false && + currentScreen !is GithubStoreGraph.OnboardingScreen + ) { + navController.navigate(GithubStoreGraph.OnboardingScreen) { + popUpTo(0) { inclusive = true } + } + } + } + LaunchedEffect(deepLinkUri) { deepLinkUri?.let { uri -> when (val destination = DeepLinkParser.parse(uri)) { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt index 06ea4cd4f..f9bbb61a2 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt @@ -16,4 +16,6 @@ data class MainState( val currentFontTheme: FontTheme = FontTheme.CUSTOM, val isScrollbarEnabled: Boolean = false, val contentWidth: ContentWidth = ContentWidth.COMPACT, + /** First-launch onboarding state. `null` while the flow is still loading. */ + val onboardingComplete: Boolean? = null, ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt index b51b6af55..26df1389a 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt @@ -74,6 +74,14 @@ class MainViewModel( } } + viewModelScope.launch { + tweaksRepository + .getOnboardingComplete() + .collect { complete -> + _state.update { it.copy(onboardingComplete = complete) } + } + } + viewModelScope.launch { tweaksRepository.getScrollbarEnabled().collect { enabled -> _state.update { it.copy(isScrollbarEnabled = enabled) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index 8199069cf..775ab7627 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -11,6 +11,7 @@ import zed.rainxch.details.presentation.DetailsViewModel import zed.rainxch.devprofile.presentation.DeveloperProfileViewModel import zed.rainxch.favourites.presentation.FavouritesViewModel import zed.rainxch.githubstore.app.announcements.AnnouncementsViewModel +import zed.rainxch.githubstore.app.onboarding.OnboardingViewModel import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel import zed.rainxch.home.presentation.HomeViewModel import zed.rainxch.profile.presentation.ProfileViewModel @@ -107,6 +108,7 @@ val viewModelsModule = viewModelOf(::HostTokensViewModel) viewModelOf(::WhatsNewViewModel) viewModelOf(::AnnouncementsViewModel) + viewModelOf(::OnboardingViewModel) viewModel { MirrorPickerViewModel( mirrorRepository = get(), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 6fb5e5a6e..32a99137b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -256,6 +256,19 @@ fun AppNavigation( ) } + composable { + zed.rainxch.githubstore.app.onboarding.OnboardingRoot( + onNavigateToSignIn = { + navController.navigate(GithubStoreGraph.AuthenticationScreen) + }, + onNavigateToHome = { + navController.navigate(GithubStoreGraph.HomeScreen) { + popUpTo(0) { inclusive = true } + } + }, + ) + } + composable { FavouritesRoot( onNavigateBack = { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt index fed4b5bb1..6f372e5dc 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt @@ -49,7 +49,10 @@ fun DesktopDrawer( modifier: Modifier = Modifier, ) { val cs = MaterialTheme.colorScheme - val items = BottomNavigationUtils.items() + // Library (AppsScreen) is Android-only: Installer + PackageMonitor + Shizuku + // don't exist on Desktop, so the screen has nothing to manage. Filter it out + // of the drawer entirely rather than show an empty stub. + val items = BottomNavigationUtils.items().filterNot { it.screen == GithubStoreGraph.AppsScreen } Column( modifier = modifier diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingAction.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingAction.kt new file mode 100644 index 000000000..1010fcc00 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingAction.kt @@ -0,0 +1,24 @@ +package zed.rainxch.githubstore.app.onboarding + +import zed.rainxch.core.domain.model.AppTheme +import zed.rainxch.core.domain.model.ThemeMode + +sealed interface OnboardingAction { + data class OnPaletteSelected( + val palette: AppTheme, + ) : OnboardingAction + + data class OnModeSelected( + val mode: ThemeMode, + ) : OnboardingAction + + data object OnNextClick : OnboardingAction + + data object OnBackClick : OnboardingAction + + data object OnSkipStepClick : OnboardingAction + + data object OnSignInClick : OnboardingAction + + data object OnFinishClick : OnboardingAction +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingEvent.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingEvent.kt new file mode 100644 index 000000000..d3f20c0ff --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingEvent.kt @@ -0,0 +1,7 @@ +package zed.rainxch.githubstore.app.onboarding + +sealed interface OnboardingEvent { + data object NavigateToSignIn : OnboardingEvent + + data object NavigateToHome : OnboardingEvent +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt new file mode 100644 index 000000000..699fbe429 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -0,0 +1,344 @@ +package zed.rainxch.githubstore.app.onboarding + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.model.AppTheme +import zed.rainxch.core.domain.model.ThemeMode +import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.core.presentation.utils.primaryColor +import zed.rainxch.core.presentation.vocabulary.CookieShape +import zed.rainxch.core.presentation.vocabulary.Squiggle + +@Composable +fun OnboardingRoot( + onNavigateToSignIn: () -> Unit, + onNavigateToHome: () -> Unit, + viewModel: OnboardingViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsState() + ObserveAsEvents(viewModel.events) { event -> + when (event) { + OnboardingEvent.NavigateToSignIn -> onNavigateToSignIn() + OnboardingEvent.NavigateToHome -> onNavigateToHome() + } + } + OnboardingScreen(state = state, onAction = viewModel::onAction) +} + +@Composable +fun OnboardingScreen( + state: OnboardingState, + onAction: (OnboardingAction) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StepIndicator(total = state.steps.size, currentIndex = state.currentIndex) + Spacer(Modifier.height(32.dp)) + + AnimatedContent( + targetState = state.currentStep, + transitionSpec = { + ( + slideInHorizontally { it } + fadeIn() togetherWith + slideOutHorizontally { -it } + fadeOut() + ) + }, + label = "onboarding-step", + modifier = Modifier.weight(1f).fillMaxWidth(), + ) { step -> + when (step) { + OnboardingStep.PALETTE -> StepPalette(state, onAction) + OnboardingStep.SIGN_IN -> StepSignIn(onAction) + OnboardingStep.PERMISSIONS -> StepPermissions(onAction) + } + } + + ActionRow(state, onAction) + } +} + +@Composable +private fun StepIndicator( + total: Int, + currentIndex: Int, +) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + repeat(total) { i -> + val active = i <= currentIndex + Box( + modifier = + Modifier + .size(width = if (i == currentIndex) 24.dp else 8.dp, height = 8.dp) + .clip(CircleShape) + .background( + if (active) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outlineVariant + }, + ), + ) + } + } +} + +@Composable +private fun StepPalette( + state: OnboardingState, + onAction: (OnboardingAction) -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Text( + text = "Pick your palette", + style = + MaterialTheme.typography.displaySmall.copy( + fontFamily = fraunces, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Squiggle() + Text( + text = "Four palettes × Light, Dark, AMOLED, System.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + AppTheme.entries.forEach { palette -> + PaletteSwatch( + palette = palette, + isSelected = state.selectedPalette == palette, + onClick = { onAction(OnboardingAction.OnPaletteSelected(palette)) }, + ) + } + } + Spacer(Modifier.height(16.dp)) + ModeRow( + selected = state.selectedMode, + onSelect = { onAction(OnboardingAction.OnModeSelected(it)) }, + ) + } +} + +@Composable +private fun PaletteSwatch( + palette: AppTheme, + isSelected: Boolean, + onClick: () -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.clickable(onClick = onClick), + ) { + Box( + modifier = + Modifier + .size(64.dp) + .clip(CookieShape) + .background(palette.primaryColor) + .border( + width = if (isSelected) 3.dp else 0.dp, + color = MaterialTheme.colorScheme.onSurface, + shape = CookieShape, + ), + ) + Text( + text = palette.name.lowercase().replaceFirstChar { it.uppercaseChar() }, + color = + if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + style = + MaterialTheme.typography.labelMedium.copy( + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + ), + ) + } +} + +@Composable +private fun ModeRow( + selected: ThemeMode, + onSelect: (ThemeMode) -> Unit, +) { + Row( + modifier = + Modifier + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + ThemeMode.entries.forEach { mode -> + val isActive = mode == selected + Box( + modifier = + Modifier + .clip(Radii.chip) + .background(if (isActive) MaterialTheme.colorScheme.primary else Color.Transparent) + .clickable { onSelect(mode) } + .padding(horizontal = 12.dp, vertical = 6.dp), + ) { + Text( + text = mode.name.lowercase().replaceFirstChar { it.uppercaseChar() }, + color = + if (isActive) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + style = MaterialTheme.typography.labelMedium, + ) + } + } + } +} + +@Composable +private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Box( + modifier = + Modifier + .size(96.dp) + .clip(CookieShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Text( + text = "G", + color = MaterialTheme.colorScheme.onPrimary, + fontFamily = fraunces, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold, + fontSize = 48.sp, + ) + } + Text( + text = "Sign in with GitHub", + style = + MaterialTheme.typography.headlineSmall.copy( + fontFamily = fraunces, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Squiggle() + Text( + text = "Stars, profile, and rate-limit headroom. Optional — skip to browse anonymously.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + PrimaryButton(onClick = { onAction(OnboardingAction.OnSignInClick) }) { + Text("Sign in") + } + } +} + +@Composable +private fun StepPermissions(onAction: (OnboardingAction) -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Text( + text = "Two quick prompts", + style = + MaterialTheme.typography.headlineSmall.copy( + fontFamily = fraunces, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Squiggle() + Text( + text = "Notifications for update alerts. Install-from-unknown-sources for non-Play APKs. Both optional.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun ActionRow( + state: OnboardingState, + onAction: (OnboardingAction) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + if (state.currentStep == OnboardingStep.SIGN_IN || state.currentStep == OnboardingStep.PERMISSIONS) { + OutlineButton(onClick = { onAction(OnboardingAction.OnSkipStepClick) }) { + Text("Skip") + } + } else { + Spacer(Modifier.size(80.dp)) + } + PrimaryButton(onClick = { onAction(OnboardingAction.OnNextClick) }) { + Text(if (state.isLast) "Get started" else "Next") + } + } +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt new file mode 100644 index 000000000..bca458d31 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt @@ -0,0 +1,21 @@ +package zed.rainxch.githubstore.app.onboarding + +import androidx.compose.runtime.Stable +import zed.rainxch.core.domain.model.AppTheme +import zed.rainxch.core.domain.model.ThemeMode + +/** Onboarding step enum. Android shows all three; Desktop skips Permissions. */ +enum class OnboardingStep { PALETTE, SIGN_IN, PERMISSIONS } + +@Stable +data class OnboardingState( + val steps: List = listOf(OnboardingStep.PALETTE, OnboardingStep.SIGN_IN), + val currentIndex: Int = 0, + val selectedPalette: AppTheme = AppTheme.NORD, + val selectedMode: ThemeMode = ThemeMode.SYSTEM, + val isAndroid: Boolean = false, +) { + val currentStep: OnboardingStep get() = steps[currentIndex] + val isFirst: Boolean get() = currentIndex == 0 + val isLast: Boolean get() = currentIndex == steps.lastIndex +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingViewModel.kt new file mode 100644 index 000000000..37911046b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingViewModel.kt @@ -0,0 +1,88 @@ +package zed.rainxch.githubstore.app.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import zed.rainxch.core.domain.getPlatform +import zed.rainxch.core.domain.model.Platform +import zed.rainxch.core.domain.repository.TweaksRepository + +class OnboardingViewModel( + private val tweaksRepository: TweaksRepository, +) : ViewModel() { + private val isAndroid = getPlatform() == Platform.ANDROID + + private val _state = + MutableStateFlow( + OnboardingState( + steps = + if (isAndroid) { + listOf(OnboardingStep.PALETTE, OnboardingStep.SIGN_IN, OnboardingStep.PERMISSIONS) + } else { + listOf(OnboardingStep.PALETTE, OnboardingStep.SIGN_IN) + }, + isAndroid = isAndroid, + ), + ) + val state = _state.asStateFlow() + + private val _events = Channel() + val events = _events.receiveAsFlow() + + fun onAction(action: OnboardingAction) { + when (action) { + is OnboardingAction.OnPaletteSelected -> { + _state.update { it.copy(selectedPalette = action.palette) } + viewModelScope.launch { tweaksRepository.setThemeColor(action.palette) } + } + + is OnboardingAction.OnModeSelected -> { + _state.update { it.copy(selectedMode = action.mode) } + viewModelScope.launch { tweaksRepository.setThemeMode(action.mode) } + } + + OnboardingAction.OnNextClick -> { + advance() + } + + OnboardingAction.OnBackClick -> { + retreat() + } + + OnboardingAction.OnSkipStepClick -> { + advance() + } + + OnboardingAction.OnSignInClick -> { + viewModelScope.launch { + _events.send(OnboardingEvent.NavigateToSignIn) + } + } + + OnboardingAction.OnFinishClick -> { + viewModelScope.launch { + tweaksRepository.setOnboardingComplete(true) + _events.send(OnboardingEvent.NavigateToHome) + } + } + } + } + + private fun advance() { + val s = _state.value + if (s.isLast) { + onAction(OnboardingAction.OnFinishClick) + } else { + _state.update { it.copy(currentIndex = it.currentIndex + 1) } + } + } + + private fun retreat() { + _state.update { if (it.isFirst) it else it.copy(currentIndex = it.currentIndex - 1) } + } +} diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index 9e4114271..f27f560f4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -103,6 +103,13 @@ class TweaksRepositoryImpl( } } + override fun getOnboardingComplete(): Flow = gatedGetFlow(K_ONBOARDING_COMPLETE, false) + + override suspend fun setOnboardingComplete(complete: Boolean) { + migrationDeferred.await() + ksafe.safePut(K_ONBOARDING_COMPLETE, complete) + } + override fun getFontTheme(): Flow = gatedGetFlow(K_FONT, "").map { FontTheme.fromName(it.ifEmpty { null }) } @@ -500,6 +507,7 @@ class TweaksRepositoryImpl( private const val K_SHOW_ALL_PLATFORMS = "show_all_platforms" private const val K_BATTERY_OPT_PROMPT_DISMISSED = "battery_opt_prompt_dismissed" private const val K_LAST_SEEN_WHATS_NEW_VERSION_CODE = "last_seen_whats_new_version_code" + private const val K_ONBOARDING_COMPLETE = "onboarding_complete" private const val K_ANNOUNCEMENTS_DISMISSED_IDS = "announcements_dismissed_ids" private const val K_ANNOUNCEMENTS_ACKNOWLEDGED_IDS = "announcements_acknowledged_ids" private const val K_ANNOUNCEMENTS_MUTED_CATEGORIES = "announcements_muted_categories" diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 6dcbcdcff..6e3c6e2cb 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -33,6 +33,11 @@ interface TweaksRepository { suspend fun setThemeMode(mode: ThemeMode) + /** One-shot first-launch onboarding completion flag (D17). */ + fun getOnboardingComplete(): Flow + + suspend fun setOnboardingComplete(complete: Boolean) + fun getFontTheme(): Flow suspend fun setFontTheme(fontTheme: FontTheme) From 5245385274bc54f4ec26ddb50ef05050d62ee0c2 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:47:54 +0500 Subject: [PATCH 015/172] fix(onboarding): one-shot persistence read + system bars padding --- .../zed/rainxch/githubstore/MainViewModel.kt | 15 ++++++++++----- .../app/onboarding/OnboardingScreen.kt | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt index 26df1389a..f45fb92b3 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import zed.rainxch.core.domain.repository.AuthenticationState @@ -75,11 +76,15 @@ class MainViewModel( } viewModelScope.launch { - tweaksRepository - .getOnboardingComplete() - .collect { complete -> - _state.update { it.copy(onboardingComplete = complete) } - } + // One-shot read of the persisted onboarding flag. Collecting the + // full flow causes a race on Desktop (and any platform whose KSafe + // backend emits the `false` default before the persisted `true` + // lands): the App.kt redirect fires on the default emission and + // pushes the user into onboarding every launch. `.first()` blocks + // until the gated flow yields the actual disk value, then we + // update state exactly once. + val complete = tweaksRepository.getOnboardingComplete().first() + _state.update { it.copy(onboardingComplete = complete) } } viewModelScope.launch { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index 699fbe429..1563634bb 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -72,6 +73,7 @@ fun OnboardingScreen( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) + .systemBarsPadding() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { From 093eac437cda66511db9e0c380006142049f4e7b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 10:58:16 +0500 Subject: [PATCH 016/172] feat(onboarding): wire real permission requests + listed rows --- .../OnboardingPermissions.android.kt | 113 ++++++++++++++++++ .../app/onboarding/OnboardingPermissions.kt | 30 +++++ .../app/onboarding/OnboardingScreen.kt | 100 +++++++++++++++- .../onboarding/OnboardingPermissions.jvm.kt | 18 +++ 4 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt create mode 100644 composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.jvm.kt diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt new file mode 100644 index 000000000..3c41c5ac2 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt @@ -0,0 +1,113 @@ +package zed.rainxch.githubstore.app.onboarding + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner + +actual class OnboardingPermissionsController internal constructor( + private val context: android.content.Context, + private val notifications: MutableState, + private val installSources: MutableState, + private val launchNotifications: () -> Unit, +) { + actual val notificationsGranted: State = notifications + actual val installSourcesGranted: State = installSources + + actual fun requestNotifications() { + // POST_NOTIFICATIONS only requestable runtime on Android 13+. On older + // releases the permission was install-time, so callers should already + // see `granted = true` and never hit this branch. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + launchNotifications() + } else { + notifications.value = true + } + } + + actual fun requestInstallSources() { + // REQUEST_INSTALL_PACKAGES is a Settings toggle, not a runtime prompt. + // Deep-link to the per-app screen; the on-resume observer below re-reads + // `canRequestPackageInstalls()` once the user returns. + val intent = + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = Uri.parse("package:${context.packageName}") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + runCatching { context.startActivity(intent) } + } + + fun refresh() { + notifications.value = readNotificationsGranted(context) + installSources.value = readInstallSourcesGranted(context) + } +} + +@Composable +actual fun rememberOnboardingPermissionsController(): OnboardingPermissionsController { + val context = LocalContext.current + val notifications = remember { mutableStateOf(readNotificationsGranted(context)) } + val installSources = remember { mutableStateOf(readInstallSourcesGranted(context)) } + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + notifications.value = granted + } + + val controller = + remember { + OnboardingPermissionsController( + context = context, + notifications = notifications, + installSources = installSources, + launchNotifications = { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + }, + ) + } + + // Re-read on resume so the install-sources Settings toggle reflects back + // when the user returns from the OS Settings screen. + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) controller.refresh() + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + return controller +} + +private fun readNotificationsGranted(context: android.content.Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED +} + +private fun readInstallSourcesGranted(context: android.content.Context): Boolean { + // canRequestPackageInstalls() exists since API 26 (Oreo). GHS minSdk is 26 + // (per top CLAUDE.md), so we don't need to gate on Build.VERSION_CODES.O. + return context.packageManager.canRequestPackageInstalls() +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt new file mode 100644 index 000000000..8556a2f2c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt @@ -0,0 +1,30 @@ +package zed.rainxch.githubstore.app.onboarding + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State + +/** + * Two-permission controller used by the Onboarding Permissions step (Android only). + * Wraps the platform's permission APIs so the composable layer stays expect-free. + * + * - [notificationsGranted]: `POST_NOTIFICATIONS` (Android 13+). On older API levels + * resolves to `true` immediately — the permission was install-time before T. + * - [installSourcesGranted]: `REQUEST_INSTALL_PACKAGES` settings toggle. There is + * no runtime prompt on Android — the user has to flip a Settings switch; we + * deep-link them there via [requestInstallSources] and re-read on resume. + * + * Desktop's actual is a no-op stub: both states report `true`, request methods do + * nothing. The Permissions step is skipped on Desktop anyway (D17) but the type + * exists in common so the screen composable can be platform-agnostic. + */ +expect class OnboardingPermissionsController { + val notificationsGranted: State + val installSourcesGranted: State + + fun requestNotifications() + + fun requestInstallSources() +} + +@Composable +expect fun rememberOnboardingPermissionsController(): OnboardingPermissionsController diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index 1563634bb..f09f36987 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -21,6 +21,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.outlined.DownloadForOffline +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,6 +45,7 @@ import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.presentation.components.buttons.OutlineButton import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.components.buttons.TintedButton import zed.rainxch.core.presentation.theme.fraunces import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents @@ -80,6 +86,8 @@ fun OnboardingScreen( StepIndicator(total = state.steps.size, currentIndex = state.currentIndex) Spacer(Modifier.height(32.dp)) + val permissionsController = rememberOnboardingPermissionsController() + AnimatedContent( targetState = state.currentStep, transitionSpec = { @@ -94,7 +102,7 @@ fun OnboardingScreen( when (step) { OnboardingStep.PALETTE -> StepPalette(state, onAction) OnboardingStep.SIGN_IN -> StepSignIn(onAction) - OnboardingStep.PERMISSIONS -> StepPermissions(onAction) + OnboardingStep.PERMISSIONS -> StepPermissions(permissionsController) } } @@ -296,10 +304,12 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { } @Composable -private fun StepPermissions(onAction: (OnboardingAction) -> Unit) { +private fun StepPermissions(controller: OnboardingPermissionsController) { + val notificationsGranted by controller.notificationsGranted + val installSourcesGranted by controller.installSourcesGranted Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( text = "Two quick prompts", @@ -314,11 +324,93 @@ private fun StepPermissions(onAction: (OnboardingAction) -> Unit) { ) Squiggle() Text( - text = "Notifications for update alerts. Install-from-unknown-sources for non-Play APKs. Both optional.", + text = "Both optional. You can flip these later in Tweaks.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) + PermissionRow( + icon = Icons.Outlined.Notifications, + label = "Notifications", + description = "Update alerts and install progress", + isGranted = notificationsGranted, + onAllowClick = { controller.requestNotifications() }, + ) + PermissionRow( + icon = Icons.Outlined.DownloadForOffline, + label = "Install unknown apps", + description = "Required to install APKs outside Play", + isGranted = installSourcesGranted, + onAllowClick = { controller.requestInstallSources() }, + ) + } +} + +@Composable +private fun PermissionRow( + icon: androidx.compose.ui.graphics.vector.ImageVector, + label: String, + description: String, + isGranted: Boolean, + onAllowClick: () -> Unit, +) { + val cs = MaterialTheme.colorScheme + Row( + modifier = + Modifier + .fillMaxWidth() + .clip(Radii.card) + .background(cs.surfaceContainer) + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = + Modifier + .size(40.dp) + .clip(CircleShape) + .background(if (isGranted) cs.tertiaryContainer else cs.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isGranted) cs.tertiary else cs.primary, + modifier = Modifier.size(22.dp), + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + color = cs.onSurface, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), + ) + Text( + text = description, + color = cs.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + ) + } + if (isGranted) { + Box( + modifier = + Modifier + .size(36.dp) + .clip(CircleShape) + .background(cs.tertiary), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Granted", + tint = cs.onPrimary, + modifier = Modifier.size(20.dp), + ) + } + } else { + TintedButton(onClick = onAllowClick) { Text("Allow") } + } } } diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.jvm.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.jvm.kt new file mode 100644 index 000000000..41a8256fa --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.jvm.kt @@ -0,0 +1,18 @@ +package zed.rainxch.githubstore.app.onboarding + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +actual class OnboardingPermissionsController { + actual val notificationsGranted: State = mutableStateOf(true) + actual val installSourcesGranted: State = mutableStateOf(true) + + actual fun requestNotifications() = Unit + + actual fun requestInstallSources() = Unit +} + +@Composable +actual fun rememberOnboardingPermissionsController(): OnboardingPermissionsController = remember { OnboardingPermissionsController() } From 1da7c3afa6ac09ba36d8b32ec1caa99799bf92c4 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 11:54:05 +0500 Subject: [PATCH 017/172] feat(home): multi-section state, actions, drop legacy mappers --- .../rainxch/home/presentation/HomeAction.kt | 46 ++- .../rainxch/home/presentation/HomeState.kt | 30 +- .../components/HomeFilterChips.kt | 360 ------------------ .../presentation/utils/HomeCategoryMapper.kt | 42 -- 4 files changed, 41 insertions(+), 437 deletions(-) delete mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeFilterChips.kt delete mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/utils/HomeCategoryMapper.kt diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt index c415a27b4..974138d53 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt @@ -2,15 +2,11 @@ package zed.rainxch.home.presentation import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.model.GithubRepoSummaryUi -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.domain.model.TopicCategory sealed interface HomeAction { - data object Refresh : HomeAction + data object OnRefreshClick : HomeAction - data object Retry : HomeAction - - data object LoadMore : HomeAction + data object OnRetry : HomeAction data object OnSearchClick : HomeAction @@ -18,34 +14,38 @@ sealed interface HomeAction { data object OnAppsClick : HomeAction - data object OnTogglePlatformPopup : HomeAction + data object OnPlatformPopupOpen : HomeAction + + data object OnPlatformPopupDismiss : HomeAction data object OnSelectAllPlatforms : HomeAction - data class OnShareClick( - val repo: GithubRepoSummaryUi, + data class OnPlatformToggle( + val platform: DiscoveryPlatform, ) : HomeAction - data class SwitchCategory( - val category: HomeCategory, + data class OnPlatformsSelected( + val platforms: Set, ) : HomeAction - data class SwitchTopic( - val topic: TopicCategory, + data class OnRepoClick( + val repo: GithubRepoSummaryUi, ) : HomeAction - data class TogglePlatform( - val platform: DiscoveryPlatform, + data class OnRepoLongClick( + val repoId: Long, ) : HomeAction - data class OnRepositoryClick( - val repo: GithubRepoSummaryUi, - ) : HomeAction + data object OnActionSheetDismiss : HomeAction - data class OnRepositoryDeveloperClick( + data class OnDeveloperClick( val username: String, ) : HomeAction + data class OnShareClick( + val repo: GithubRepoSummaryUi, + ) : HomeAction + data class OnHideRepository( val repo: GithubRepoSummaryUi, ) : HomeAction @@ -61,4 +61,12 @@ sealed interface HomeAction { data class OnMarkAsUnseen( val repoId: Long, ) : HomeAction + + data object OnSeeAllHot : HomeAction + + data object OnSeeAllTrending : HomeAction + + data object OnSeeAllPopular : HomeAction + + data object OnSeeAllStarred : HomeAction } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt index f0f636e8b..6ea5db3f1 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt @@ -1,33 +1,31 @@ package zed.rainxch.home.presentation +import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.InstalledApp import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.domain.model.TopicCategory +@Stable data class HomeState( - val repos: ImmutableList = persistentListOf(), + val hot: ImmutableList = persistentListOf(), + val trending: ImmutableList = persistentListOf(), + val popular: ImmutableList = persistentListOf(), + val starred: ImmutableList = persistentListOf(), val installedApps: ImmutableList = persistentListOf(), - val isLoading: Boolean = false, - val isLoadingMore: Boolean = false, - val isLoadingTopicSupplement: Boolean = false, + val isHotLoading: Boolean = false, + val isTrendingLoading: Boolean = false, + val isPopularLoading: Boolean = false, + val isStarredLoading: Boolean = false, val errorMessage: String? = null, - val hasMorePages: Boolean = true, - val currentCategory: HomeCategory = HomeCategory.TRENDING, - /** Empty set means "no topic filter" (show all topics). */ - val selectedTopics: Set = emptySet(), - val isAppsSectionVisible: Boolean = false, - val isUpdateAvailable: Boolean = false, - /** - * Empty set means "all platforms" (no filter). Anything else is the - * subset the user explicitly opted into via the platform popup. - */ val selectedPlatforms: Set = emptySet(), val isPlatformPopupVisible: Boolean = false, + val isAppsSectionVisible: Boolean = false, + val isUpdateAvailable: Boolean = false, val isHideSeenEnabled: Boolean = false, + val isUserSignedIn: Boolean = false, val seenRepoIds: Set = emptySet(), val hiddenRepoIds: Set = emptySet(), + val actionSheetRepoId: Long? = null, ) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeFilterChips.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeFilterChips.kt deleted file mode 100644 index 4452539e0..000000000 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeFilterChips.kt +++ /dev/null @@ -1,360 +0,0 @@ -package zed.rainxch.home.presentation.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.presentation.utils.displayText - -@Composable -fun LiquidGlassCategoryChips( - categories: List, - selectedCategory: HomeCategory, - onCategorySelected: (HomeCategory) -> Unit, - modifier: Modifier = Modifier, -) { - val density = LocalDensity.current - - val isDarkTheme = - !MaterialTheme.colorScheme.background - .luminance() - .let { it > 0.5f } - - val itemPositions = remember { mutableMapOf>() } - var selectedItemPos by remember { mutableStateOf?>(null) } - - val selectedIndex = categories.indexOf(selectedCategory) - - val rowPaddingDp = 6.dp - val rowPaddingPx = with(density) { rowPaddingDp.toPx() } - val insetPx = with(density) { 2.dp.toPx() } - - val indicatorX = remember { Animatable(0f) } - val indicatorWidth = remember { Animatable(0f) } - - LaunchedEffect(selectedIndex, selectedItemPos) { - val raw = selectedItemPos ?: itemPositions[selectedIndex] ?: return@LaunchedEffect - val targetX = raw.first + rowPaddingPx - insetPx - val targetW = raw.second + insetPx * 2f - - launch { - // E4.2: tween over spring for the chip-row indicator — same - // jitter rationale as BottomNavigation. Selecting a category - // hundreds of times per session shouldn't overshoot. - indicatorX.animateTo( - targetValue = targetX, - animationSpec = tween(durationMillis = 220), - ) - } - launch { - indicatorWidth.animateTo( - targetValue = targetW, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ) - } - } - - val glassHighColor = - if (isDarkTheme) Color.White.copy(alpha = .14f) else Color.White.copy(alpha = .50f) - val glassLowColor = - if (isDarkTheme) Color.White.copy(alpha = .05f) else Color.White.copy(alpha = .18f) - val specularColor = - if (isDarkTheme) Color.White.copy(alpha = .20f) else Color.White.copy(alpha = .55f) - val innerGlowColor = - if (isDarkTheme) Color.White.copy(alpha = .04f) else Color.White.copy(alpha = .10f) - val borderColor = if (isDarkTheme) Color.White.copy(alpha = .10f) else Color.Transparent - - val containerShape = RoundedCornerShape(20.dp) - - Box( - modifier = - modifier - .fillMaxWidth() - .clip(containerShape) - .background(MaterialTheme.colorScheme.surfaceContainer) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant, - shape = containerShape, - ), - ) { - Box( - modifier = - Modifier - .matchParentSize() - .drawBehind { - if (indicatorWidth.value > 0f) { - val pillTop = 5.dp.toPx() - val pillHeight = size.height - 10.dp.toPx() - val pillCorner = 14.dp.toPx() - val pillRadius = CornerRadius(pillCorner) - - if (isDarkTheme) { - drawRoundRect( - color = borderColor, - topLeft = - Offset( - indicatorX.value - .5.dp.toPx(), - pillTop - .5.dp.toPx(), - ), - size = - Size( - indicatorWidth.value + 1.dp.toPx(), - pillHeight + 1.dp.toPx(), - ), - cornerRadius = pillRadius, - style = Stroke(width = 1.dp.toPx()), - ) - } - - drawRoundRect( - brush = - Brush.verticalGradient( - colors = listOf(glassHighColor, glassLowColor), - startY = pillTop, - endY = pillTop + pillHeight, - ), - topLeft = Offset(indicatorX.value, pillTop), - size = Size(indicatorWidth.value, pillHeight), - cornerRadius = pillRadius, - ) - - val specLeft = indicatorX.value + indicatorWidth.value * .12f - val specWidth = indicatorWidth.value * .76f - drawRoundRect( - brush = - Brush.horizontalGradient( - colors = - listOf( - Color.Transparent, - specularColor, - specularColor.copy(alpha = specularColor.alpha * .6f), - Color.Transparent, - ), - startX = specLeft, - endX = specLeft + specWidth, - ), - topLeft = Offset(specLeft, pillTop + 1.dp.toPx()), - size = Size(specWidth, 1.5.dp.toPx()), - cornerRadius = CornerRadius(1.dp.toPx()), - ) - - drawRoundRect( - brush = - Brush.verticalGradient( - colors = listOf(Color.Transparent, innerGlowColor), - startY = pillTop + pillHeight - 6.dp.toPx(), - endY = pillTop + pillHeight, - ), - topLeft = - Offset( - indicatorX.value + 6.dp.toPx(), - pillTop + pillHeight - 5.dp.toPx(), - ), - size = Size(indicatorWidth.value - 12.dp.toPx(), 4.dp.toPx()), - cornerRadius = CornerRadius(2.dp.toPx()), - ) - - val edgeAlpha = if (isDarkTheme) .06f else .12f - drawRoundRect( - brush = - Brush.horizontalGradient( - colors = - listOf( - Color.White.copy(alpha = edgeAlpha), - Color.Transparent, - ), - startX = indicatorX.value, - endX = indicatorX.value + 4.dp.toPx(), - ), - topLeft = Offset(indicatorX.value, pillTop + 4.dp.toPx()), - size = Size(3.dp.toPx(), pillHeight - 8.dp.toPx()), - cornerRadius = CornerRadius(1.5.dp.toPx()), - ) - drawRoundRect( - brush = - Brush.horizontalGradient( - colors = - listOf( - Color.Transparent, - Color.White.copy(alpha = edgeAlpha), - ), - startX = indicatorX.value + indicatorWidth.value - 4.dp.toPx(), - endX = indicatorX.value + indicatorWidth.value, - ), - topLeft = - Offset( - indicatorX.value + indicatorWidth.value - 3.dp.toPx(), - pillTop + 4.dp.toPx(), - ), - size = Size(3.dp.toPx(), pillHeight - 8.dp.toPx()), - cornerRadius = CornerRadius(1.5.dp.toPx()), - ) - } - }, - ) - - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = rowPaddingDp, vertical = 3.dp), - horizontalArrangement = Arrangement.spacedBy(0.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - categories.forEachIndexed { index, category -> - LiquidGlassCategoryChip( - category = category, - isSelected = category == selectedCategory, - onSelect = { onCategorySelected(category) }, - modifier = Modifier.weight(1f), - onPositioned = { x, width -> - itemPositions[index] = x to width - if (index == selectedIndex) { - selectedItemPos = x to width - } - if (index == selectedIndex && indicatorWidth.value == 0f) { - indicatorX.snapTo(x + rowPaddingPx - insetPx) - indicatorWidth.snapTo(width + insetPx * 2f) - } - }, - ) - } - } - } -} - -@Composable -private fun LiquidGlassCategoryChip( - category: HomeCategory, - isSelected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, - onPositioned: suspend (x: Float, width: Float) -> Unit, -) { - val scope = rememberCoroutineScope() - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val pressScale by animateFloatAsState( - targetValue = if (isPressed) 0.90f else 1f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMediumLow, - ), - label = "chipPressScale", - ) - - val selectedAlpha by animateFloatAsState( - targetValue = if (isSelected) 1f else 0f, - animationSpec = tween(200), - label = "selectedAlpha", - ) - - val textColor by animateColorAsState( - targetValue = - if (isSelected) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = .65f) - }, - animationSpec = tween(250), - label = "chipTextColor", - ) - - Box( - modifier = - modifier - .clip(CircleShape) - .clickable( - interactionSource = interactionSource, - indication = null, - ) { onSelect() } - .onGloballyPositioned { coordinates -> - val x = coordinates.positionInParent().x - val width = coordinates.size.width.toFloat() - scope.launch { onPositioned(x, width) } - }.graphicsLayer { - scaleX = pressScale - scaleY = pressScale - }.padding(vertical = 8.dp), - contentAlignment = Alignment.Center, - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = category.displayText(), - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Medium, - ), - color = textColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier.graphicsLayer { alpha = 1f - selectedAlpha }, - ) - Text( - text = category.displayText(), - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Bold, - ), - color = textColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier.graphicsLayer { alpha = selectedAlpha }, - ) - } - } -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/utils/HomeCategoryMapper.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/utils/HomeCategoryMapper.kt deleted file mode 100644 index 91c3a9157..000000000 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/utils/HomeCategoryMapper.kt +++ /dev/null @@ -1,42 +0,0 @@ -package zed.rainxch.home.presentation.utils - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Code -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.MusicNote -import androidx.compose.material.icons.outlined.Speed -import androidx.compose.material.icons.outlined.Wifi -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.domain.model.HomeCategory.* -import zed.rainxch.home.domain.model.TopicCategory - -@Composable -fun HomeCategory.displayText(): String = - when (this) { - TRENDING -> stringResource(Res.string.home_category_trending) - HOT_RELEASE -> stringResource(Res.string.home_category_hot_release) - MOST_POPULAR -> stringResource(Res.string.home_category_most_popular) - } - -@Composable -fun TopicCategory.displayText(): String = - when (this) { - TopicCategory.PRIVACY -> stringResource(Res.string.home_topic_privacy) - TopicCategory.MEDIA -> stringResource(Res.string.home_topic_media) - TopicCategory.PRODUCTIVITY -> stringResource(Res.string.home_topic_productivity) - TopicCategory.NETWORKING -> stringResource(Res.string.home_topic_networking) - TopicCategory.DEV_TOOLS -> stringResource(Res.string.home_topic_dev_tools) - } - -fun TopicCategory.icon(): ImageVector = - when (this) { - TopicCategory.PRIVACY -> Icons.Outlined.Lock - TopicCategory.MEDIA -> Icons.Outlined.MusicNote - TopicCategory.PRODUCTIVITY -> Icons.Outlined.Speed - TopicCategory.NETWORKING -> Icons.Outlined.Wifi - TopicCategory.DEV_TOOLS -> Icons.Outlined.Code - } From 740214d289833e5f2b213ceca780e0eb28e0222e Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 11:54:09 +0500 Subject: [PATCH 018/172] feat(home): parallel section fetches, drop pagination --- .../home/presentation/HomeViewModel.kt | 811 +++++++----------- 1 file changed, 332 insertions(+), 479 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 8caba7ac6..b1aa3a04c 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -6,14 +6,11 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn @@ -22,7 +19,10 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.DiscoveryPlatform +import zed.rainxch.core.domain.model.GithubRepoSummary +import zed.rainxch.core.domain.model.GithubUser import zed.rainxch.core.domain.model.Platform +import zed.rainxch.core.domain.model.StarredRepository as StarredRepositoryModel import zed.rainxch.core.domain.model.hasActualUpdate import zed.rainxch.core.domain.model.isReallyInstalled import zed.rainxch.core.domain.repository.FavouritesRepository @@ -36,10 +36,7 @@ import zed.rainxch.core.domain.utils.ShareManager import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.utils.toUi import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.domain.model.TopicCategory import zed.rainxch.home.domain.repository.HomeRepository -import zed.rainxch.home.presentation.HomeEvent.* import zed.rainxch.profile.domain.repository.ProfileRepository class HomeViewModel( @@ -57,14 +54,8 @@ class HomeViewModel( private val profileRepository: ProfileRepository, ) : ViewModel() { private var hasLoadedInitialData = false - private var currentJob: Job? = null - private var switchCategoryJob: Job? = null - private var topicSupplementJob: Job? = null - private var nextPageIndex = 1 - - // Cached so each repo mapping doesn't re-hit ProfileRepository (which - // walks a suspend cache + DataStore on every call). Refreshed by the - // observer below — login/logout flips badges without restarting the VM. + private var loadJob: Job? = null + @Volatile private var currentUserLogin: String? = null private val _state = MutableStateFlow(HomeState()) @@ -76,7 +67,7 @@ class HomeViewModel( syncSystemState() loadPlatform() - loadRepos(isInitial = true) + refreshAllSections(isInitial = true) observeInstalledApps() observeFavourites() observeStarredRepos() @@ -96,363 +87,47 @@ class HomeViewModel( private val _events = Channel(capacity = Channel.BUFFERED) val events = _events.receiveAsFlow() - private fun syncSystemState() { - viewModelScope.launch { - try { - val result = syncInstalledAppsUseCase() - if (result.isFailure) { - logger.warn("Initial sync had issues: ${result.exceptionOrNull()?.message}") + fun onAction(action: HomeAction) { + when (action) { + HomeAction.OnRefreshClick -> { + viewModelScope.launch { + syncInstalledAppsUseCase() + refreshAllSections(isInitial = false) } - } catch (e: Exception) { - logger.error("Initial sync failed: ${e.message}") } - } - } - private fun loadPlatform() { - _state.update { - it.copy(isAppsSectionVisible = platform == Platform.ANDROID) - } - } + HomeAction.OnRetry -> refreshAllSections(isInitial = true) - private fun observeInstalledApps() { - viewModelScope.launch { - installedAppsRepository.getAllInstalledApps().collect { installedApps -> - val installedMap = installedApps.groupBy { it.repoId } - _state.update { current -> - current.copy( - repos = - current.repos - .map { homeRepo -> - val apps = installedMap[homeRepo.repository.id].orEmpty() - homeRepo.copy( - isInstalled = apps.any { it.isReallyInstalled() }, - isUpdateAvailable = apps.any { it.hasActualUpdate() }, - ) - }.toImmutableList(), - isUpdateAvailable = installedMap.values.flatten().any { it.hasActualUpdate() }, - ) - } + HomeAction.OnPlatformPopupOpen -> { + _state.update { it.copy(isPlatformPopupVisible = true) } } - } - } - private fun observeDiscoveryPlatforms() { - viewModelScope.launch { - tweaksRepository.getDiscoveryPlatforms().collect { platforms -> - _state.update { - it.copy( - selectedPlatforms = platforms, - ) - } + HomeAction.OnPlatformPopupDismiss -> { + _state.update { it.copy(isPlatformPopupVisible = false) } } - } - } - - private fun loadRepos( - isInitial: Boolean = false, - category: HomeCategory? = null, - platforms: Set? = null, - topics: Set? = null, - topicsExplicitlySet: Boolean = false, - ): Job? { - currentJob?.cancel() - topicSupplementJob?.cancel() - - if (_state.value.isLoading || _state.value.isLoadingMore) { - logger.debug("Already loading, skipping...") - return null - } - if (isInitial) { - nextPageIndex = 1 - } - - val targetCategory = category ?: _state.value.currentCategory - val targetPlatformsDeferred = - viewModelScope.async { - tweaksRepository.getDiscoveryPlatforms().first() + is HomeAction.OnPlatformToggle -> { + val target = _state.value.selectedPlatforms.toggle(action.platform) + applyPlatformSelection(target) } - val targetTopics = if (topicsExplicitlySet) topics.orEmpty() else _state.value.selectedTopics - - logger.debug("Loading repos: category=$targetCategory, topics=$targetTopics, page=$nextPageIndex, isInitial=$isInitial") - - return viewModelScope - .launch { - val targetPlatforms = platforms ?: targetPlatformsDeferred.await() - - if (platforms != null) { - tweaksRepository.setDiscoveryPlatforms(targetPlatforms) - } - - _state.update { - it.copy( - isLoading = isInitial, - isLoadingMore = !isInitial, - errorMessage = null, - selectedPlatforms = targetPlatforms, - currentCategory = targetCategory, - selectedTopics = targetTopics, - repos = if (isInitial) persistentListOf() else it.repos, - ) - } - - try { - val flow = - when (targetCategory) { - HomeCategory.TRENDING -> { - homeRepository.getTrendingRepositories( - platforms = targetPlatforms, - page = nextPageIndex, - ) - } - - HomeCategory.HOT_RELEASE -> { - homeRepository.getHotReleaseRepositories( - platforms = targetPlatforms, - page = nextPageIndex, - ) - } - - HomeCategory.MOST_POPULAR -> { - homeRepository.getMostPopular( - platforms = targetPlatforms, - page = nextPageIndex, - ) - } - } - - flow.collect { paginatedRepos -> - logger.debug( - "Received ${paginatedRepos.repos.size} repos, hasMore=${paginatedRepos.hasMore}, nextPage=${paginatedRepos.nextPageIndex}", - ) - - this@HomeViewModel.nextPageIndex = paginatedRepos.nextPageIndex - - val repos = - if (targetTopics.isEmpty()) { - paginatedRepos.repos - } else { - paginatedRepos.repos.filter { repo -> - targetTopics.any { topic -> - topic.matchesRepo(repo.topics, repo.description, repo.name) - } - } - } - - val newReposWithStatus = mapReposToUi(repos) - - _state.update { currentState -> - val rawList = currentState.repos + newReposWithStatus - val uniqueList = rawList.distinctBy { it.repository.fullName } - - currentState.copy( - repos = uniqueList.toImmutableList(), - hasMorePages = paginatedRepos.hasMore, - errorMessage = - if (uniqueList.isEmpty() && !paginatedRepos.hasMore) { - getString(Res.string.no_repositories_found) - } else { - null - }, - ) - } - } - - logger.debug("Flow completed") - _state.update { - it.copy(isLoading = false, isLoadingMore = false) - } - if (targetTopics.isNotEmpty() && isInitial) { - loadTopicSupplement(targetTopics, targetPlatforms) - } - } catch (t: Throwable) { - if (t is CancellationException) { - logger.debug("Load cancelled (expected)") - throw t - } - - logger.error("Load failed: ${t.message}") - _state.update { - it.copy( - isLoading = false, - isLoadingMore = false, - errorMessage = - t.message - ?: getString(Res.string.home_failed_to_load_repositories), - ) - } - } - }.also { - currentJob = it + is HomeAction.OnPlatformsSelected -> { + applyPlatformSelection(action.platforms) } - } - private fun loadTopicSupplement( - topics: Set, - platforms: Set, - ) { - topicSupplementJob?.cancel() - topicSupplementJob = - viewModelScope.launch { - _state.update { it.copy(isLoadingTopicSupplement = true) } - - try { - // Phase 1: Pre-fetched cached topic repos (instant, no API cost). - // Run mirror fetches per selected topic in parallel, merge once. - val cachedRepos = - coroutineScope { - topics.map { topic -> - async { - homeRepository - .getTopicRepositories(topic = topic, platforms = platforms) - .firstOrNull() - ?.repos - .orEmpty() - } - }.awaitAll().flatten() - } - if (cachedRepos.isNotEmpty()) { - val cachedReposWithStatus = mapReposToUi(cachedRepos) - _state.update { currentState -> - val merged = - (currentState.repos + cachedReposWithStatus) - .distinctBy { it.repository.fullName } - currentState.copy(repos = merged.toImmutableList()) - } - logger.debug("Loaded ${cachedRepos.size} cached topic repos for $topics") - } - - // Phase 2: Live GitHub search (fills gaps). One search per selected - // topic so each topic's keywords AND together correctly inside its - // own query, while results from different topics OR via merge. - topics.forEach { topic -> - homeRepository - .searchByTopic( - searchKeywords = topic.searchKeywords, - platforms = platforms, - page = 1, - ).collect { paginatedRepos -> - val newReposWithStatus = mapReposToUi(paginatedRepos.repos) - - _state.update { currentState -> - val merged = - (currentState.repos + newReposWithStatus) - .distinctBy { it.repository.fullName } - - currentState.copy( - repos = merged.toImmutableList(), - hasMorePages = currentState.hasMorePages || paginatedRepos.hasMore, - ) - } - } - } - } catch (t: Throwable) { - if (t is CancellationException) throw t - logger.warn("Topic supplement search failed: ${t.message}") - } finally { - _state.update { it.copy(isLoadingTopicSupplement = false) } - } - } - } - - private suspend fun mapReposToUi(repos: List): List { - val installedAppsMap = - installedAppsRepository - .getAllInstalledApps() - .first() - .groupBy { it.repoId } - - val favoritesMap = - favouritesRepository - .getAllFavorites() - .first() - .associateBy { it.repoId } - - val starredReposMap = - starredRepository - .getAllStarred() - .first() - .associateBy { it.repoId } - - val seenIds = _state.value.seenRepoIds - val currentLogin = currentUserLogin - - return repos.map { repo -> - val apps = installedAppsMap[repo.id].orEmpty() - val favourite = favoritesMap[repo.id] - val starred = starredReposMap[repo.id] - - DiscoveryRepositoryUi( - isInstalled = apps.any { it.isReallyInstalled() }, - isFavourite = favourite != null, - isStarred = starred != null, - isSeen = repo.id in seenIds, - isCurrentUserOwner = - currentLogin != null && - repo.owner.login.equals(currentLogin, ignoreCase = true), - isUpdateAvailable = apps.any { it.hasActualUpdate() }, - repository = repo.toUi(), - ) - } - } - - fun onAction(action: HomeAction) { - when (action) { - HomeAction.Refresh -> { - viewModelScope.launch { - syncInstalledAppsUseCase() - nextPageIndex = 1 - loadRepos(isInitial = true) - } - } - - HomeAction.Retry -> { - nextPageIndex = 1 - loadRepos(isInitial = true) - } - - HomeAction.LoadMore -> { - logger.debug( - "LoadMore action: isLoading=${_state.value.isLoading}, isLoadingMore=${_state.value.isLoadingMore}, hasMore=${_state.value.hasMorePages}", - ) - - if (!_state.value.isLoadingMore && !_state.value.isLoading && _state.value.hasMorePages) { - loadRepos(isInitial = false) - } + HomeAction.OnSelectAllPlatforms -> { + val current = _state.value.selectedPlatforms + val target = + if (current.isEmpty()) setOf(devicePlatformAsDiscovery()) else emptySet() + applyPlatformSelection(target) } - is HomeAction.SwitchTopic -> { - val current = _state.value.selectedTopics - val target = - if (action.topic in current) current - action.topic else current + action.topic - if (target != current) { - nextPageIndex = 1 - switchCategoryJob?.cancel() - switchCategoryJob = - viewModelScope.launch { - loadRepos( - isInitial = true, - topics = target, - topicsExplicitlySet = true, - )?.join() ?: return@launch - _events.send(HomeEvent.OnScrollToListTop) - } - } + is HomeAction.OnRepoLongClick -> { + _state.update { it.copy(actionSheetRepoId = action.repoId) } } - is HomeAction.SwitchCategory -> { - if (_state.value.currentCategory != action.category) { - nextPageIndex = 1 - switchCategoryJob?.cancel() - switchCategoryJob = - viewModelScope.launch { - loadRepos(isInitial = true, category = action.category)?.join() - ?: return@launch - _events.send(HomeEvent.OnScrollToListTop) - } - } + HomeAction.OnActionSheetDismiss -> { + _state.update { it.copy(actionSheetRepoId = null) } } is HomeAction.OnShareClick -> { @@ -461,68 +136,15 @@ class HomeViewModel( shareManager.shareText("https://github-store.org/app?repo=${action.repo.fullName}") }.onFailure { t -> logger.error("Failed to share link: ${t.message}") - _events.send( - OnMessage(getString(Res.string.failed_to_share_link)), - ) + _events.send(HomeEvent.OnMessage(getString(Res.string.failed_to_share_link))) return@launch } - if (platform != Platform.ANDROID) { - _events.send(OnMessage(getString(Res.string.link_copied_to_clipboard))) - } - } - } - - is HomeAction.TogglePlatform -> { - val current = _state.value.selectedPlatforms - val target = current.toggle(action.platform) - if (target != current) { - nextPageIndex = 1 - switchCategoryJob?.cancel() - switchCategoryJob = - viewModelScope.launch { - loadRepos(isInitial = true, platforms = target)?.join() - ?: return@launch - _events.send(OnScrollToListTop) - } - } - } - - HomeAction.OnSelectAllPlatforms -> { - val target = - if (_state.value.selectedPlatforms.isEmpty()) { - setOf(devicePlatformAsDiscovery()) - } else { - emptySet() + _events.send(HomeEvent.OnMessage(getString(Res.string.link_copied_to_clipboard))) } - if (target != _state.value.selectedPlatforms) { - nextPageIndex = 1 - switchCategoryJob?.cancel() - switchCategoryJob = - viewModelScope.launch { - loadRepos(isInitial = true, platforms = target)?.join() - ?: return@launch - _events.send(OnScrollToListTop) - } } } - HomeAction.OnTogglePlatformPopup -> { - _state.update { - it.copy( - isPlatformPopupVisible = !it.isPlatformPopupVisible, - ) - } - } - - is HomeAction.OnRepositoryClick -> { - // Handled in composable - } - - is HomeAction.OnRepositoryDeveloperClick -> { - // Handled in composable - } - is HomeAction.OnHideRepository -> { val repo = action.repo viewModelScope.launch { @@ -586,50 +208,188 @@ class HomeViewModel( } } - HomeAction.OnSearchClick -> { - // Handled in composable + HomeAction.OnSearchClick, + HomeAction.OnSettingsClick, + HomeAction.OnAppsClick, + is HomeAction.OnRepoClick, + is HomeAction.OnDeveloperClick, + HomeAction.OnSeeAllHot, + HomeAction.OnSeeAllTrending, + HomeAction.OnSeeAllPopular, + HomeAction.OnSeeAllStarred -> Unit // Handled in composable + } + } + + private fun applyPlatformSelection(target: Set) { + if (target == _state.value.selectedPlatforms) return + viewModelScope.launch { + tweaksRepository.setDiscoveryPlatforms(target) + _state.update { it.copy(selectedPlatforms = target, isPlatformPopupVisible = false) } + refreshAllSections(isInitial = true) + _events.send(HomeEvent.OnScrollToListTop) + } + } + + private fun refreshAllSections(isInitial: Boolean) { + loadJob?.cancel() + loadJob = + viewModelScope.launch { + val platforms = tweaksRepository.getDiscoveryPlatforms().first() + _state.update { + it.copy( + selectedPlatforms = platforms, + isHotLoading = true, + isTrendingLoading = true, + isPopularLoading = true, + isStarredLoading = _state.value.isUserSignedIn, + errorMessage = null, + hot = if (isInitial) persistentListOf() else _state.value.hot, + trending = if (isInitial) persistentListOf() else _state.value.trending, + popular = if (isInitial) persistentListOf() else _state.value.popular, + starred = if (isInitial) persistentListOf() else _state.value.starred, + ) + } + + coroutineScope { + launch { loadHot(platforms) } + launch { loadTrending(platforms) } + launch { loadPopular(platforms) } + launch { loadStarred() } + } } + } - HomeAction.OnSettingsClick -> { - // Handled in composable + private suspend fun loadHot(platforms: Set) { + try { + val page = homeRepository.getHotReleaseRepositories(platforms, page = 1).first() + val mapped = mapReposToUi(page.repos) + _state.update { + it.copy( + hot = mapped.toImmutableList(), + isHotLoading = false, + ) + } + } catch (t: CancellationException) { + throw t + } catch (t: Throwable) { + logger.error("Hot section load failed: ${t.message}") + _state.update { + it.copy( + isHotLoading = false, + errorMessage = it.errorMessage ?: t.message + ?: getString(Res.string.home_failed_to_load_repositories), + ) } + } + } - HomeAction.OnAppsClick -> { - // Handled in composable + private suspend fun loadTrending(platforms: Set) { + try { + val page = homeRepository.getTrendingRepositories(platforms, page = 1).first() + val mapped = mapReposToUi(page.repos) + _state.update { + it.copy( + trending = mapped.toImmutableList(), + isTrendingLoading = false, + ) } + } catch (t: CancellationException) { + throw t + } catch (t: Throwable) { + logger.error("Trending section load failed: ${t.message}") + _state.update { it.copy(isTrendingLoading = false) } } } - /** - * Tap-from-`All` (empty selection) selects only the tapped platform — - * not "every other platform" — which is what users intuit from the - * popup. Tapping the only remaining platform deselects it and falls - * back to the device's own platform so the home feed never ends up - * empty. Reaching every selectable platform collapses to the `All` - * representation (empty set) to keep the chip row tidy. - */ - private fun Set.toggle(platform: DiscoveryPlatform): Set { - if (platform == DiscoveryPlatform.All) return emptySet() + private suspend fun loadPopular(platforms: Set) { + try { + val page = homeRepository.getMostPopular(platforms, page = 1).first() + val mapped = mapReposToUi(page.repos) + _state.update { + it.copy( + popular = mapped.toImmutableList(), + isPopularLoading = false, + ) + } + } catch (t: CancellationException) { + throw t + } catch (t: Throwable) { + logger.error("Popular section load failed: ${t.message}") + _state.update { it.copy(isPopularLoading = false) } + } + } - if (isEmpty()) return setOf(platform) + private suspend fun loadStarred() { + if (!_state.value.isUserSignedIn) { + _state.update { it.copy(starred = persistentListOf(), isStarredLoading = false) } + return + } + try { + runCatching { starredRepository.syncStarredRepos(forceRefresh = false) } + val top = starredRepository + .getAllStarred() + .first() + .sortedByDescending { it.stargazersCount } + .take(5) + .map { it.toSummary() } + val mapped = mapReposToUi(top) + _state.update { + it.copy( + starred = mapped.toImmutableList(), + isStarredLoading = false, + ) + } + } catch (t: CancellationException) { + throw t + } catch (t: Throwable) { + logger.error("Starred section load failed: ${t.message}") + _state.update { it.copy(isStarredLoading = false) } + } + } - val mutated = - if (platform in this) this - platform else this + platform + private fun syncSystemState() { + viewModelScope.launch { + try { + val result = syncInstalledAppsUseCase() + if (result.isFailure) { + logger.warn("Initial sync had issues: ${result.exceptionOrNull()?.message}") + } + } catch (e: Exception) { + logger.error("Initial sync failed: ${e.message}") + } + } + } - return when { - mutated.size == DiscoveryPlatform.selectablePlatforms.size -> emptySet() - mutated.isEmpty() -> setOf(devicePlatformAsDiscovery()) - else -> mutated + private fun loadPlatform() { + _state.update { + it.copy(isAppsSectionVisible = platform == Platform.ANDROID) } } - private fun devicePlatformAsDiscovery(): DiscoveryPlatform = - when (platform) { - Platform.ANDROID -> DiscoveryPlatform.Android - Platform.WINDOWS -> DiscoveryPlatform.Windows - Platform.MACOS -> DiscoveryPlatform.Macos - Platform.LINUX -> DiscoveryPlatform.Linux + private fun observeInstalledApps() { + viewModelScope.launch { + installedAppsRepository.getAllInstalledApps().collect { installedApps -> + val installedMap = installedApps.groupBy { it.repoId } + _state.update { current -> + current.copy( + hot = current.hot.restamp(installedMap), + trending = current.trending.restamp(installedMap), + popular = current.popular.restamp(installedMap), + starred = current.starred.restamp(installedMap), + isUpdateAvailable = installedMap.values.flatten().any { it.hasActualUpdate() }, + ) + } + } + } + } + + private fun observeDiscoveryPlatforms() { + viewModelScope.launch { + tweaksRepository.getDiscoveryPlatforms().collect { platforms -> + _state.update { it.copy(selectedPlatforms = platforms) } + } } + } private fun observeSeenRepos() { viewModelScope.launch { @@ -637,11 +397,10 @@ class HomeViewModel( _state.update { current -> current.copy( seenRepoIds = ids, - repos = - current.repos - .map { repo -> - repo.copy(isSeen = repo.repository.id in ids) - }.toImmutableList(), + hot = current.hot.restampSeen(ids), + trending = current.trending.restampSeen(ids), + popular = current.popular.restampSeen(ids), + starred = current.starred.restampSeen(ids), ) } } @@ -651,10 +410,6 @@ class HomeViewModel( private fun observeHiddenRepos() { viewModelScope.launch { hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> - // Track IDs only — mirror the `seenRepoIds` pattern so - // unhiding restores the repo to the visible list without - // a refresh. `HomeRoot.visibleRepos` filters at render - // time using these IDs. _state.update { it.copy(hiddenRepoIds = ids) } } } @@ -672,23 +427,23 @@ class HomeViewModel( viewModelScope.launch { profileRepository.getUser().collect { user -> currentUserLogin = user?.username - // Re-stamp `isCurrentUserOwner` on already-loaded repos so - // logging in (or switching accounts) immediately flips - // badges without forcing a full reload. + val signedIn = user != null + val previouslySignedIn = _state.value.isUserSignedIn val login = user?.username _state.update { current -> current.copy( - repos = - current.repos - .map { repo -> - repo.copy( - isCurrentUserOwner = - login != null && - repo.repository.owner.login.equals(login, ignoreCase = true), - ) - }.toImmutableList(), + isUserSignedIn = signedIn, + hot = current.hot.restampOwner(login), + trending = current.trending.restampOwner(login), + popular = current.popular.restampOwner(login), + starred = current.starred.restampOwner(login), ) } + if (signedIn != previouslySignedIn) { + if (signedIn) loadStarred() else _state.update { + it.copy(starred = persistentListOf(), isStarredLoading = false) + } + } } } } @@ -696,16 +451,13 @@ class HomeViewModel( private fun observeFavourites() { viewModelScope.launch { favouritesRepository.getAllFavorites().collect { favourites -> - val favouritesMap = favourites.associateBy { it.repoId } + val keys = favourites.map { it.repoId }.toSet() _state.update { current -> current.copy( - repos = - current.repos - .map { homeRepo -> - homeRepo.copy( - isFavourite = favouritesMap.containsKey(homeRepo.repository.id), - ) - }.toImmutableList(), + hot = current.hot.restampFavourite(keys), + trending = current.trending.restampFavourite(keys), + popular = current.popular.restampFavourite(keys), + starred = current.starred.restampFavourite(keys), ) } } @@ -715,25 +467,126 @@ class HomeViewModel( private fun observeStarredRepos() { viewModelScope.launch { starredRepository.getAllStarred().collect { starredRepos -> - val starredReposById = starredRepos.associateBy { it.repoId } + val keys = starredRepos.map { it.repoId }.toSet() _state.update { current -> current.copy( - repos = - current.repos - .map { homeRepo -> - homeRepo.copy( - isStarred = starredReposById.containsKey(homeRepo.repository.id), - ) - }.toImmutableList(), + hot = current.hot.restampStarred(keys), + trending = current.trending.restampStarred(keys), + popular = current.popular.restampStarred(keys), + starred = current.starred.restampStarred(keys), ) } } } } + private suspend fun mapReposToUi(repos: List): List { + val installedAppsMap = + installedAppsRepository.getAllInstalledApps().first().groupBy { it.repoId } + val favoritesMap = + favouritesRepository.getAllFavorites().first().associateBy { it.repoId } + val starredReposMap = + starredRepository.getAllStarred().first().associateBy { it.repoId } + val seenIds = _state.value.seenRepoIds + val currentLogin = currentUserLogin + + return repos.map { repo -> + val apps = installedAppsMap[repo.id].orEmpty() + val favourite = favoritesMap[repo.id] + val starred = starredReposMap[repo.id] + + DiscoveryRepositoryUi( + isInstalled = apps.any { it.isReallyInstalled() }, + isFavourite = favourite != null, + isStarred = starred != null, + isSeen = repo.id in seenIds, + isCurrentUserOwner = + currentLogin != null && + repo.owner.login.equals(currentLogin, ignoreCase = true), + isUpdateAvailable = apps.any { it.hasActualUpdate() }, + repository = repo.toUi(), + ) + } + } + + private fun Set.toggle(platform: DiscoveryPlatform): Set { + if (platform == DiscoveryPlatform.All) return emptySet() + if (isEmpty()) return setOf(platform) + val mutated = if (platform in this) this - platform else this + platform + return when { + mutated.size == DiscoveryPlatform.selectablePlatforms.size -> emptySet() + mutated.isEmpty() -> setOf(devicePlatformAsDiscovery()) + else -> mutated + } + } + + private fun devicePlatformAsDiscovery(): DiscoveryPlatform = + when (platform) { + Platform.ANDROID -> DiscoveryPlatform.Android + Platform.WINDOWS -> DiscoveryPlatform.Windows + Platform.MACOS -> DiscoveryPlatform.Macos + Platform.LINUX -> DiscoveryPlatform.Linux + } + override fun onCleared() { super.onCleared() - currentJob?.cancel() - topicSupplementJob?.cancel() + loadJob?.cancel() } } + +private fun kotlinx.collections.immutable.ImmutableList.restamp( + installedMap: Map>, +) = map { repo -> + val apps = installedMap[repo.repository.id].orEmpty() + repo.copy( + isInstalled = apps.any { it.isReallyInstalled() }, + isUpdateAvailable = apps.any { it.hasActualUpdate() }, + ) +}.toImmutableList() + +private fun kotlinx.collections.immutable.ImmutableList.restampSeen( + ids: Set, +) = map { it.copy(isSeen = it.repository.id in ids) }.toImmutableList() + +private fun kotlinx.collections.immutable.ImmutableList.restampFavourite( + ids: Set, +) = map { it.copy(isFavourite = it.repository.id in ids) }.toImmutableList() + +private fun kotlinx.collections.immutable.ImmutableList.restampStarred( + ids: Set, +) = map { it.copy(isStarred = it.repository.id in ids) }.toImmutableList() + +private fun kotlinx.collections.immutable.ImmutableList.restampOwner( + login: String?, +) = map { repo -> + repo.copy( + isCurrentUserOwner = + login != null && repo.repository.owner.login.equals(login, ignoreCase = true), + ) +}.toImmutableList() + +private fun StarredRepositoryModel.toSummary(): GithubRepoSummary = + GithubRepoSummary( + id = repoId, + name = repoName, + fullName = "$repoOwner/$repoName", + owner = GithubUser( + id = 0L, + login = repoOwner, + avatarUrl = repoOwnerAvatarUrl, + htmlUrl = "https://github.com/$repoOwner", + ), + description = repoDescription, + defaultBranch = "main", + htmlUrl = repoUrl, + stargazersCount = stargazersCount, + forksCount = forksCount, + language = primaryLanguage, + topics = emptyList(), + releasesUrl = "$repoUrl/releases", + updatedAt = "", + isFork = false, + availablePlatforms = emptyList(), + downloadCount = 0, + sourceHost = null, + ) From 1d282bf77eb4b75c901b1cb482335806ff072406 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 11:54:18 +0500 Subject: [PATCH 019/172] feat(home): section components and long-press sheet --- .../presentation/components/cards/RowCard.kt | 3 +- .../components/HomeTimeHelpers.kt | 16 ++ .../presentation/components/HomeTopBar.kt | 92 +++++++++++ .../presentation/components/HotCardItem.kt | 116 ++++++++++++++ .../home/presentation/components/LeadCard.kt | 130 +++++++++++++++ .../components/PlatformFilterMenu.kt | 74 +++++++++ .../components/RepositoryActionsSheet.kt | 134 ++++++++++++++++ .../presentation/components/StarredRowItem.kt | 91 +++++++++++ .../components/TrendingRowItem.kt | 150 ++++++++++++++++++ 9 files changed, 805 insertions(+), 1 deletion(-) create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/RepositoryActionsSheet.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt index 638f0701a..d14425eb3 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -22,7 +23,7 @@ import zed.rainxch.core.presentation.theme.tokens.Radii fun RowCard( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, - content: @Composable () -> Unit, + content: @Composable RowScope.() -> Unit, ) { Row( modifier = modifier diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt new file mode 100644 index 000000000..f63939208 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt @@ -0,0 +1,16 @@ +package zed.rainxch.home.presentation.components + +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +internal fun daysSinceIso(isoInstant: String): Int { + val trimmed = isoInstant.trim() + if (trimmed.isEmpty()) return Int.MAX_VALUE + val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return Int.MAX_VALUE + val nowMs = Clock.System.now().toEpochMilliseconds() + val diffMs = nowMs - parsed.toEpochMilliseconds() + if (diffMs <= 0L) return 0 + return (diffMs / 86_400_000L).toInt() +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt new file mode 100644 index 000000000..b095b985f --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt @@ -0,0 +1,92 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.IconButton +import zed.rainxch.core.presentation.vocabulary.CookieShape +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.app_name + +@Composable +fun HomeTopBar( + onSearchClick: () -> Unit, + onSettingsClick: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable (RowScope.() -> Unit) = {}, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + CookieMark() + Text( + text = stringResource(Res.string.app_name), + style = MaterialTheme.typography.titleLarge.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + actions() + IconButton(onClick = onSearchClick) { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton(onClick = onSettingsClick) { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +private fun CookieMark() { + androidx.compose.foundation.layout.Box( + modifier = Modifier + .size(32.dp) + .clip(CookieShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Text( + text = "G", + style = MaterialTheme.typography.titleMedium.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ), + color = MaterialTheme.colorScheme.onPrimary, + ) + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt new file mode 100644 index 000000000..ee3919a3c --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -0,0 +1,116 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import zed.rainxch.core.domain.model.DiscoveryPlatform +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.cards.CompactCard +import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.core.presentation.vocabulary.FreshnessRing +import zed.rainxch.core.presentation.vocabulary.PlatformGlyph +import zed.rainxch.core.presentation.vocabulary.PlatformKind +import zed.rainxch.core.presentation.vocabulary.StarTier +import zed.rainxch.core.presentation.vocabulary.TopicGlyph + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HotCardItem( + repo: DiscoveryRepositoryUi, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val r = repo.repository + val days = daysSinceIso(r.updatedAt) + + Box( + modifier = modifier + .width(280.dp) + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { + CompactCard { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.fillMaxWidth(), + ) { + FreshnessRing(daysSinceRelease = days, sizeDp = 48) { + GitHubStoreImage( + imageModel = { r.owner.avatarUrl }, + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = r.name, + style = MaterialTheme.typography.titleMedium.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + StarTier(stars = r.stargazersCount) + } + } + Spacer(Modifier.height(8.dp)) + if (!r.description.isNullOrBlank()) { + Text( + text = r.description!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(10.dp)) + } + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 0.5.dp, + ) + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + r.topics.orEmpty().take(2).forEach { topic -> + TopicGlyph(topic = topic, sizeDp = 14) + } + Box(Modifier.weight(1f)) + listOf( + DiscoveryPlatform.Android to PlatformKind.ANDROID, + DiscoveryPlatform.Windows to PlatformKind.WINDOWS, + DiscoveryPlatform.Macos to PlatformKind.MACOS, + DiscoveryPlatform.Linux to PlatformKind.LINUX, + ).forEach { (plat, kind) -> + if (plat in r.availablePlatforms) { + PlatformGlyph(kind = kind, supported = true, sizeDp = 14) + } + } + } + } + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt new file mode 100644 index 000000000..8fb1cfe93 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -0,0 +1,130 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.core.presentation.components.cards.LeadHeroCard +import zed.rainxch.core.presentation.vocabulary.AppAccentResolver +import zed.rainxch.core.presentation.vocabulary.FreshnessRing +import zed.rainxch.core.presentation.vocabulary.Heartbeat +import zed.rainxch.core.presentation.vocabulary.StarTier +import zed.rainxch.core.presentation.vocabulary.TopicGlyph + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LeadCard( + repo: DiscoveryRepositoryUi, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val r = repo.repository + val isDark = isSystemInDarkTheme() + val accent = AppAccentResolver.resolve( + backendHex = null, + topics = r.topics.orEmpty(), + primaryLanguage = r.language, + ) + val days = daysSinceIso(r.updatedAt) + + Box( + modifier = modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { + LeadHeroCard(accent = accent, isDark = isDark) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier.fillMaxWidth(), + ) { + FreshnessRing(daysSinceRelease = days, sizeDp = 80) { + GitHubStoreImage( + imageModel = { r.owner.avatarUrl }, + modifier = Modifier.size(80.dp).clip(CircleShape), + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = r.name, + style = MaterialTheme.typography.headlineSmall.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 26.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(2.dp)) + Text( + text = r.owner.login, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + StarTier(stars = r.stargazersCount) + Heartbeat(daysSinceCommit = days) + r.topics.orEmpty().take(3).forEach { topic -> + TopicGlyph(topic = topic, sizeDp = 14) + } + } + } + } + Spacer(Modifier.height(14.dp)) + if (!r.description.isNullOrBlank()) { + Text( + text = r.description!!, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(14.dp)) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + PrimaryButton(onClick = onClick) { + Text(text = ctaLabel(repo)) + } + } + } + } +} + +internal fun ctaLabel(repo: DiscoveryRepositoryUi): String = when { + repo.isUpdateAvailable -> "Update" + repo.isInstalled -> "Open" + else -> "Get" +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt new file mode 100644 index 000000000..1e655945f --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt @@ -0,0 +1,74 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import zed.rainxch.core.domain.model.DiscoveryPlatform +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.utils.toLabel + +@Composable +fun PlatformFilterMenu( + expanded: Boolean, + selectedPlatforms: Set, + onDismiss: () -> Unit, + onSelectAll: () -> Unit, + onToggle: (DiscoveryPlatform) -> Unit, +) { + GhsDropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + PlatformRow( + label = DiscoveryPlatform.All.toLabel(), + isSelected = selectedPlatforms.isEmpty(), + onClick = onSelectAll, + ) + DiscoveryPlatform.selectablePlatforms.forEach { platform -> + PlatformRow( + label = platform.toLabel(), + isSelected = platform in selectedPlatforms, + onClick = { onToggle(platform) }, + ) + } + } +} + +@Composable +private fun PlatformRow( + label: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (isSelected) { + Icon( + imageVector = Icons.Default.Done, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp), + ) + } + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/RepositoryActionsSheet.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/RepositoryActionsSheet.kt new file mode 100644 index 000000000..5778f01c9 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/RepositoryActionsSheet.kt @@ -0,0 +1,134 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.OpenInBrowser +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet +import zed.rainxch.core.presentation.model.GithubRepoSummaryUi +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.hide_repository +import zed.rainxch.githubstore.core.presentation.res.mark_as_unviewed +import zed.rainxch.githubstore.core.presentation.res.mark_as_viewed +import zed.rainxch.githubstore.core.presentation.res.open_on_github +import zed.rainxch.githubstore.core.presentation.res.share_repository + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RepositoryActionsSheet( + repository: GithubRepoSummaryUi, + isSeen: Boolean, + onDismiss: () -> Unit, + onShare: () -> Unit, + onOpenOnGithub: () -> Unit, + onToggleSeen: () -> Unit, + onHide: () -> Unit, +) { + GhsBottomSheet(onDismissRequest = onDismiss) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + GitHubStoreImage( + imageModel = { repository.owner.avatarUrl }, + modifier = Modifier.size(36.dp).clip(CircleShape), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = repository.fullName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + repository.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + Spacer(Modifier.height(4.dp)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + SheetRow( + label = stringResource(Res.string.share_repository), + icon = Icons.Default.Share, + onClick = onShare, + ) + SheetRow( + label = stringResource(Res.string.open_on_github), + icon = Icons.Default.OpenInBrowser, + onClick = onOpenOnGithub, + ) + SheetRow( + label = if (isSeen) stringResource(Res.string.mark_as_unviewed) + else stringResource(Res.string.mark_as_viewed), + icon = Icons.Outlined.Visibility, + onClick = onToggleSeen, + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + SheetRow( + label = stringResource(Res.string.hide_repository), + icon = Icons.Default.VisibilityOff, + onClick = onHide, + tint = MaterialTheme.colorScheme.error, + ) + } +} + +@Composable +private fun SheetRow( + label: String, + icon: ImageVector, + onClick: () -> Unit, + tint: Color = MaterialTheme.colorScheme.onSurface, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 4.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Icon(imageVector = icon, contentDescription = null, tint = tint) + Text( + text = label, + color = tint, + style = MaterialTheme.typography.bodyLarge, + ) + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt new file mode 100644 index 000000000..cd1186808 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt @@ -0,0 +1,91 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.cards.RowCard +import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.core.presentation.vocabulary.StarTier + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StarredRowItem( + repo: DiscoveryRepositoryUi, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val r = repo.repository + val starTint = Color(0xFFC49652) + + Box( + modifier = modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { + RowCard { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(starTint.copy(alpha = 0.18f)), + contentAlignment = Alignment.Center, + ) { + GitHubStoreImage( + imageModel = { r.owner.avatarUrl }, + modifier = Modifier.size(36.dp).clip(CircleShape), + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = r.name, + style = MaterialTheme.typography.titleSmall.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + tint = starTint, + modifier = Modifier.size(12.dp), + ) + StarTier(stars = r.stargazersCount, size = 10) + } + } + OutlineButton(onClick = onClick) { + Text(text = ctaLabel(repo)) + } + } + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt new file mode 100644 index 000000000..e6e9d63da --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt @@ -0,0 +1,150 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.domain.model.DiscoveryPlatform +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.cards.RowCard +import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.core.presentation.vocabulary.PlatformGlyph +import zed.rainxch.core.presentation.vocabulary.PlatformKind +import zed.rainxch.core.presentation.vocabulary.StarTier + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TrendingRowItem( + repo: DiscoveryRepositoryUi, + rank: Int, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + RankRowItem( + repo = repo, + rank = rank, + rankColor = MaterialTheme.colorScheme.primary, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun PopularRowItem( + repo: DiscoveryRepositoryUi, + rank: Int, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + RankRowItem( + repo = repo, + rank = rank, + rankColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f), + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun RankRowItem( + repo: DiscoveryRepositoryUi, + rank: Int, + rankColor: Color, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val r = repo.repository + + Box( + modifier = modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { + RowCard { + Text( + text = "#$rank", + color = rankColor, + style = MaterialTheme.typography.titleMedium.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + modifier = Modifier.width(38.dp), + ) + GitHubStoreImage( + imageModel = { r.owner.avatarUrl }, + modifier = Modifier.size(36.dp).clip(CircleShape), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = r.name, + style = MaterialTheme.typography.titleSmall.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + StarTier(stars = r.stargazersCount, size = 10) + Text( + text = r.owner.login, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + listOf( + DiscoveryPlatform.Android to PlatformKind.ANDROID, + DiscoveryPlatform.Windows to PlatformKind.WINDOWS, + DiscoveryPlatform.Macos to PlatformKind.MACOS, + DiscoveryPlatform.Linux to PlatformKind.LINUX, + ).forEach { (plat, kind) -> + if (plat in r.availablePlatforms) { + PlatformGlyph(kind = kind, supported = true, sizeDp = 12) + } + } + } + OutlineButton(onClick = onClick) { + Text(text = ctaLabel(repo)) + } + } + } +} + From c1c0c40ad5b1d9dd339f9b3b9793a8875b8af543 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 11:54:22 +0500 Subject: [PATCH 020/172] feat(home): rewrite Root to multi-section feed --- .../app/navigation/AppNavigation.kt | 2 +- .../zed/rainxch/home/presentation/HomeRoot.kt | 856 ++++++------------ 2 files changed, 272 insertions(+), 586 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 32a99137b..7e38ff76b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -115,7 +115,7 @@ fun AppNavigation( ) { composable { HomeRoot( - onNavigateToSearch = { + onNavigateToSearch = { _ -> navController.navigate(GithubStoreGraph.SearchScreen()) }, onNavigateToSettings = { diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index ec3001f98..4bee4da77 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -1,127 +1,90 @@ package zed.rainxch.home.presentation -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState -import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid -import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells -import androidx.compose.foundation.lazy.staggeredgrid.items -import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Done import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntRect -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupPositionProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.components.GithubStoreButton -import zed.rainxch.core.presentation.components.RepositoryCard import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.section.SectionHeader import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents -import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.core.presentation.utils.toIcons -import zed.rainxch.core.presentation.utils.toLabel import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.home.domain.model.HomeCategory -import zed.rainxch.home.domain.model.TopicCategory -import zed.rainxch.home.presentation.components.LiquidGlassCategoryChips -import zed.rainxch.home.presentation.utils.displayText -import zed.rainxch.home.presentation.utils.icon +import zed.rainxch.home.presentation.components.HomeTopBar +import zed.rainxch.home.presentation.components.HotCardItem +import zed.rainxch.home.presentation.components.LeadCard +import zed.rainxch.home.presentation.components.PlatformFilterMenu +import zed.rainxch.home.presentation.components.PopularRowItem +import zed.rainxch.home.presentation.components.RepositoryActionsSheet +import zed.rainxch.home.presentation.components.StarredRowItem +import zed.rainxch.home.presentation.components.TrendingRowItem @Composable fun HomeRoot( onNavigateToSettings: () -> Unit, - onNavigateToSearch: () -> Unit, + onNavigateToSearch: (category: String?) -> Unit, onNavigateToApps: () -> Unit, onNavigateToDetails: (repoId: Long) -> Unit, onNavigateToDeveloperProfile: (username: String) -> Unit, viewModel: HomeViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() - val listState = rememberLazyStaggeredGridState() + val listState = rememberLazyListState() val scope = rememberCoroutineScope() val snackbarHost = remember { SnackbarHostState() } ObserveAsEvents(viewModel.events) { event -> when (event) { HomeEvent.OnScrollToListTop -> { - scope.launch { - listState.animateScrollToItem(0) - } + scope.launch { listState.animateScrollToItem(0) } } - is HomeEvent.OnMessage -> { - scope.launch { - snackbarHost.showSnackbar(event.message) - } + scope.launch { snackbarHost.showSnackbar(event.message) } } } } @@ -129,73 +92,34 @@ fun HomeRoot( HomeScreen( state = state, snackbarHost = snackbarHost, + listState = listState, onAction = { action -> when (action) { - HomeAction.OnSearchClick -> { - onNavigateToSearch() - } - - HomeAction.OnSettingsClick -> { - onNavigateToSettings() - } - - HomeAction.OnAppsClick -> { - onNavigateToApps() - } - - is HomeAction.OnRepositoryClick -> { - onNavigateToDetails(action.repo.id) - } - - is HomeAction.OnRepositoryDeveloperClick -> { - onNavigateToDeveloperProfile(action.username) - } - - else -> { - viewModel.onAction(action) - } + HomeAction.OnSearchClick -> onNavigateToSearch(null) + HomeAction.OnSettingsClick -> onNavigateToSettings() + HomeAction.OnAppsClick -> onNavigateToApps() + HomeAction.OnSeeAllHot -> onNavigateToSearch("hot") + HomeAction.OnSeeAllTrending -> onNavigateToSearch("trending") + HomeAction.OnSeeAllPopular -> onNavigateToSearch("popular") + HomeAction.OnSeeAllStarred -> onNavigateToSearch("starred") + is HomeAction.OnRepoClick -> onNavigateToDetails(action.repo.id) + is HomeAction.OnDeveloperClick -> onNavigateToDeveloperProfile(action.username) + else -> viewModel.onAction(action) } }, - listState = listState, ) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun HomeScreen( state: HomeState, snackbarHost: SnackbarHostState, + listState: LazyListState, onAction: (HomeAction) -> Unit, - listState: LazyStaggeredGridState, ) { val bottomNavHeight = LocalBottomNavigationHeight.current - val shouldLoadMore by remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItems = layoutInfo.totalItemsCount - val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() - - totalItems > 0 && - lastVisibleItem != null && - lastVisibleItem.index >= (totalItems - 5) && - !state.isLoadingMore && - !state.isLoading && - state.hasMorePages - } - } - - LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore) { - onAction(HomeAction.LoadMore) - } - } - - // Material 3's enter-always behavior: heightOffset ticks 1:1 with scroll - // delta (finger-speed tracking), snaps/flings settle naturally, and any - // upward scroll re-reveals the header instantly. See #440. - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - Scaffold( snackbarHost = { SnackbarHost( @@ -204,444 +128,239 @@ fun HomeScreen( ) }, containerColor = MaterialTheme.colorScheme.background, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { innerPadding -> - Column( - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 8.dp), - ) { - CollapsibleHeader(scrollBehavior = scrollBehavior) { - HomeTopAppBar( - selectedPlatforms = state.selectedPlatforms, - onTogglePlatformPopup = { - onAction(HomeAction.OnTogglePlatformPopup) - }, - ) - - FilterChips(state, onAction) - - TopicChips( - selectedTopics = state.selectedTopics, - onTopicSelected = { topic -> - onAction(HomeAction.SwitchTopic(topic)) - }, + val uriHandler = LocalUriHandler.current + Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + val isAnyLoading = state.isHotLoading || state.isTrendingLoading || + state.isPopularLoading || state.isStarredLoading + val sectionsAreEmpty = state.hot.isEmpty() && state.trending.isEmpty() && + state.popular.isEmpty() && state.starred.isEmpty() + + when { + isAnyLoading && sectionsAreEmpty -> LoadingState() + state.errorMessage != null && sectionsAreEmpty -> ErrorState( + message = state.errorMessage, + onRetry = { onAction(HomeAction.OnRetry) }, ) - - Spacer(modifier = Modifier.height(4.dp)) - } - - Box(Modifier.fillMaxSize()) { - LoadingState(state) - - ErrorState(state, onAction) - - MainState( + else -> FeedContent( state = state, listState = listState, onAction = onAction, ) } - } - - // Popup is hoisted out of the TopAppBar so its parent's - // `LayoutCoordinates` are the (stable) Scaffold root rather - // than the (resizing) icons row. Without this, every icon - // count change during multi-select toggles the parent layout - // pass and the Popup window briefly tears down and re-creates. - if (state.isPlatformPopupVisible) { - PlatformsPopup( - onTogglePlatformPopup = { - onAction(HomeAction.OnTogglePlatformPopup) - }, - onTogglePlatform = { - onAction(HomeAction.TogglePlatform(it)) - }, - onSelectAllPlatforms = { - onAction(HomeAction.OnSelectAllPlatforms) - }, - selectedPlatforms = state.selectedPlatforms, - ) - } - } -} - -// Custom-layout analogue of Material 3's `TopAppBarLayout`. Measures children -// at their natural size, reports a shrunk height (natural + heightOffset) so -// siblings in the outer Column reflow upward, and translates the children by -// the same offset so the header slides rather than collapsing contents onto -// each other. `heightOffsetLimit` is synced to the measured natural height so -// `scrollBehavior` knows the full collapsible range. -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CollapsibleHeader( - scrollBehavior: TopAppBarScrollBehavior, - content: @Composable () -> Unit, -) { - var naturalHeightPx by remember { mutableFloatStateOf(0f) } - LaunchedEffect(naturalHeightPx) { - if (naturalHeightPx > 0f) { - scrollBehavior.state.heightOffsetLimit = -naturalHeightPx - } - } - - Layout( - content = content, - modifier = Modifier.clipToBounds(), - ) { measurables, constraints -> - val loose = constraints.copy(minHeight = 0, maxHeight = Int.MAX_VALUE) - val placeables = measurables.map { it.measure(loose) } - val width = - placeables.maxOfOrNull { it.width } - ?.coerceAtLeast(constraints.minWidth) - ?: constraints.minWidth - val natural = placeables.sumOf { it.height } - naturalHeightPx = natural.toFloat() - val offset = scrollBehavior.state.heightOffset.toInt().coerceIn(-natural, 0) - val rendered = (natural + offset).coerceAtLeast(0) - - layout(width, rendered) { - var y = offset - placeables.forEach { p -> - p.place(0, y) - y += p.height - } - } - } -} - -@Composable -private fun TopicChips( - selectedTopics: Set, - onTopicSelected: (TopicCategory) -> Unit, -) { - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 4.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - TopicCategory.entries.forEach { topic -> - val isSelected = topic in selectedTopics - - val containerColor by animateColorAsState( - targetValue = - if (isSelected) { - MaterialTheme.colorScheme.secondaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHigh + val actionRepo = state.actionSheetRepoId + ?.let { findRepo(state, it) } + if (actionRepo != null) { + RepositoryActionsSheet( + repository = actionRepo.repository, + isSeen = actionRepo.isSeen, + onDismiss = { onAction(HomeAction.OnActionSheetDismiss) }, + onShare = { + onAction(HomeAction.OnActionSheetDismiss) + onAction(HomeAction.OnShareClick(actionRepo.repository)) }, - animationSpec = tween(250), - label = "topicChipContainer", - ) - - val labelColor by animateColorAsState( - targetValue = - if (isSelected) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant + onOpenOnGithub = { + onAction(HomeAction.OnActionSheetDismiss) + uriHandler.openUri(actionRepo.repository.htmlUrl) }, - animationSpec = tween(250), - label = "topicChipLabel", - ) - - FilterChip( - selected = isSelected, - onClick = { onTopicSelected(topic) }, - label = { - Text( - text = topic.displayText(), - style = MaterialTheme.typography.labelLarge, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - ) - }, - leadingIcon = { - Icon( - imageVector = topic.icon(), - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - colors = - FilterChipDefaults.filterChipColors( - containerColor = containerColor, - labelColor = labelColor, - iconColor = labelColor, - selectedContainerColor = containerColor, - selectedLabelColor = labelColor, - selectedLeadingIconColor = labelColor, - ), - border = - FilterChipDefaults.filterChipBorder( - borderColor = Color.Transparent, - selectedBorderColor = Color.Transparent, - enabled = true, - selected = isSelected, - ), - shape = RoundedCornerShape(12.dp), - ) + onToggleSeen = { + onAction(HomeAction.OnActionSheetDismiss) + if (actionRepo.isSeen) { + onAction(HomeAction.OnMarkAsUnseen(actionRepo.repository.id)) + } else { + onAction(HomeAction.OnMarkAsSeen(actionRepo.repository)) + } + }, + onHide = { + onAction(HomeAction.OnActionSheetDismiss) + onAction(HomeAction.OnHideRepository(actionRepo.repository)) + }, + ) + } } } } @Composable -private fun MainState( +private fun FeedContent( state: HomeState, - listState: LazyStaggeredGridState, + listState: LazyListState, onAction: (HomeAction) -> Unit, ) { val bottomNavHeight = LocalBottomNavigationHeight.current - val visibleRepos by remember( - state.repos, - state.isHideSeenEnabled, - state.seenRepoIds, - state.hiddenRepoIds, - ) { - derivedStateOf { - val hidden = state.hiddenRepoIds - val needsHideSeen = state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty() - if (hidden.isEmpty() && !needsHideSeen) { - state.repos - } else { - state.repos.filter { repo -> - repo.repository.id !in hidden && - (!needsHideSeen || repo.repository.id !in state.seenRepoIds) - } - } - } - } + val visibleHot by visibleReposState(state, state.hot) + val visibleTrending by visibleReposState(state, state.trending) + val visiblePopular by visibleReposState(state, state.popular) + val visibleStarred by visibleReposState(state, state.starred) - if (visibleRepos.isNotEmpty()) { - val isScrollbarEnabled = LocalScrollbarEnabled.current - ScrollbarContainer( - gridState = listState, - enabled = isScrollbarEnabled, - modifier = Modifier.fillMaxSize(), - ) { - LazyVerticalStaggeredGrid( + val lead = visibleHot.firstOrNull() + val hotTail = visibleHot.drop(1) + + val isScrollbarEnabled = LocalScrollbarEnabled.current + ScrollbarContainer( + listState = listState, + enabled = isScrollbarEnabled, + modifier = Modifier.fillMaxSize(), + ) { + LazyColumn( state = listState, - columns = StaggeredGridCells.Adaptive(350.dp), - verticalItemSpacing = 12.dp, - horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = - PaddingValues( - start = 8.dp, - end = 8.dp, - top = 12.dp, - bottom = bottomNavHeight + 32.dp, - ), - modifier = Modifier.fillMaxSize().arrowKeyScroll(listState, autoFocus = true), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 12.dp, + end = 12.dp, + top = 0.dp, + bottom = bottomNavHeight + 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - items( - items = visibleRepos, - key = { it.repository.id }, - contentType = { "repo" }, - ) { discoveryRepository -> - RepositoryCard( - discoveryRepositoryUi = discoveryRepository, - onClick = { - onAction(HomeAction.OnRepositoryClick(discoveryRepository.repository)) - }, - onDeveloperClick = { username -> - onAction(HomeAction.OnRepositoryDeveloperClick(username)) + item(key = "top_bar") { + HomeTopBar( + onSearchClick = { onAction(HomeAction.OnSearchClick) }, + onSettingsClick = { onAction(HomeAction.OnSettingsClick) }, + actions = { + PlatformFilterAction( + selectedPlatforms = state.selectedPlatforms, + isPlatformPopupVisible = state.isPlatformPopupVisible, + onAction = onAction, + ) }, - onShareClick = { - onAction(HomeAction.OnShareClick(discoveryRepository.repository)) - }, - onHideClick = { - onAction(HomeAction.OnHideRepository(discoveryRepository.repository)) - }, - onToggleSeen = { - if (discoveryRepository.isSeen) { - onAction(HomeAction.OnMarkAsUnseen(discoveryRepository.repository.id)) - } else { - onAction(HomeAction.OnMarkAsSeen(discoveryRepository.repository)) - } - }, - modifier = Modifier.animateItem(), ) } - if (state.isLoadingMore || state.isLoadingTopicSupplement) { - item(key = "loading_indicator") { - Box( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center, - ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - ) - - Spacer(modifier = Modifier.width(8.dp)) + if (lead != null) { + item(key = "lead_${lead.repository.id}") { + LeadCard( + repo = lead, + onClick = { onAction(HomeAction.OnRepoClick(lead.repository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(lead.repository.id)) }, + modifier = Modifier.padding(vertical = 4.dp), + ) + } + } - Text( - text = stringResource(Res.string.home_loading_more), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + if (hotTail.isNotEmpty()) { + item(key = "hot_header") { + SectionHeader( + title = "Hot releases", + subCount = hotTail.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllHot) }, + ) + } + item(key = "hot_row") { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(horizontal = 4.dp), + ) { + items(items = hotTail, key = { "hot_${it.repository.id}" }) { repo -> + HotCardItem( + repo = repo, + onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, ) } } } } - if (!state.hasMorePages && !state.isLoadingMore && !state.isLoadingTopicSupplement) { - item(key = "end_message") { - Text( - text = stringResource(Res.string.home_no_more_repositories), - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.titleMedium, + if (visibleTrending.isNotEmpty()) { + item(key = "trending_header") { + SectionHeader( + title = "Trending now", + subCount = visibleTrending.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllTrending) }, + ) + } + itemsIndexed( + items = visibleTrending, + key = { _, repo -> "trending_${repo.repository.id}" }, + ) { idx, repo -> + TrendingRowItem( + repo = repo, + rank = idx + 1, + onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, ) } } - } - } // ScrollbarContainer - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun LoadingState(state: HomeState) { - if (state.isLoading && state.repos.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularWavyProgressIndicator() - Spacer(modifier = Modifier.height(8.dp)) + if (visiblePopular.isNotEmpty()) { + item(key = "popular_header") { + SectionHeader( + title = "Most popular", + subCount = visiblePopular.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllPopular) }, + ) + } + itemsIndexed( + items = visiblePopular, + key = { _, repo -> "popular_${repo.repository.id}" }, + ) { idx, repo -> + PopularRowItem( + repo = repo, + rank = idx + 1, + onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, + ) + } + } - Text( - text = stringResource(Res.string.home_finding_repositories), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + if (state.isUserSignedIn && visibleStarred.isNotEmpty()) { + item(key = "starred_header") { + SectionHeader( + title = "From your stars", + subCount = visibleStarred.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllStarred) }, + ) + } + items(items = visibleStarred, key = { "starred_${it.repository.id}" }) { repo -> + StarredRowItem( + repo = repo, + onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, + ) + } } } } } @Composable -private fun ErrorState( - state: HomeState, +private fun PlatformFilterAction( + selectedPlatforms: Set, + isPlatformPopupVisible: Boolean, onAction: (HomeAction) -> Unit, ) { - if (state.errorMessage != null && state.repos.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, + Box { + val icons = selectedPlatformsIcons(selectedPlatforms) + Row( + modifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .clickable { onAction(HomeAction.OnPlatformPopupOpen) } + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(16.dp), - ) { - Text( - text = state.errorMessage, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - GithubStoreButton( - text = stringResource(Res.string.home_retry), - onClick = { - onAction(HomeAction.Retry) - }, + icons.forEach { icon -> + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurface, ) } } + PlatformFilterMenu( + expanded = isPlatformPopupVisible, + selectedPlatforms = selectedPlatforms, + onDismiss = { onAction(HomeAction.OnPlatformPopupDismiss) }, + onSelectAll = { onAction(HomeAction.OnSelectAllPlatforms) }, + onToggle = { onAction(HomeAction.OnPlatformToggle(it)) }, + ) } } -@Composable -private fun FilterChips( - state: HomeState, - onAction: (HomeAction) -> Unit, -) { - LiquidGlassCategoryChips( - categories = HomeCategory.entries.toList(), - selectedCategory = state.currentCategory, - onCategorySelected = { category -> - onAction(HomeAction.SwitchCategory(category)) - }, - ) -} - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun HomeTopAppBar( - selectedPlatforms: Set, - onTogglePlatformPopup: () -> Unit, -) { - TopAppBar( - navigationIcon = { - Image( - painter = painterResource(Res.drawable.app_icon), - contentDescription = null, - modifier = Modifier.size(48.dp), - contentScale = ContentScale.Crop, - ) - }, - title = { - Text( - text = stringResource(Res.string.app_name), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Black, - modifier = Modifier.padding(start = 4.dp), - maxLines = 2, - softWrap = false, - overflow = TextOverflow.Ellipsis, - ) - }, - actions = { - val icons = selectedPlatformsIcons(selectedPlatforms) - - Row( - modifier = - Modifier - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .clickable(onClick = onTogglePlatformPopup) - .padding(vertical = 4.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - icons.forEach { icon -> - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onSurface, - ) - } - } - }, - modifier = Modifier.padding(12.dp), - contentPadding = PaddingValues(), - windowInsets = WindowInsets(), - ) -} - @Composable private fun selectedPlatformsIcons( selectedPlatforms: Set, @@ -653,110 +372,77 @@ private fun selectedPlatformsIcons( .flatMap { it.toIcons() } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun PlatformsPopup( - onTogglePlatformPopup: () -> Unit, - onTogglePlatform: (DiscoveryPlatform) -> Unit, - onSelectAllPlatforms: () -> Unit, - selectedPlatforms: Set, -) { - val density = LocalDensity.current - val positionProvider = remember(density) { - WindowAnchoredTopEndPopupPositionProvider( - topPaddingPx = with(density) { 80.dp.roundToPx() }, - endPaddingPx = with(density) { 16.dp.roundToPx() }, - ) - } - Popup( - popupPositionProvider = positionProvider, - onDismissRequest = onTogglePlatformPopup, - ) { - Column( - modifier = - Modifier - .width(250.dp) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHighest), - ) { - PlatformPopupRow( - label = DiscoveryPlatform.All.toLabel(), - isSelected = selectedPlatforms.isEmpty(), - onClick = onSelectAllPlatforms, - ) - - DiscoveryPlatform.selectablePlatforms.forEach { platform -> - PlatformPopupRow( - label = platform.toLabel(), - isSelected = platform in selectedPlatforms, - onClick = { onTogglePlatform(platform) }, - ) +private fun visibleReposState( + state: HomeState, + source: kotlinx.collections.immutable.ImmutableList, +) = remember(source, state.hiddenRepoIds, state.seenRepoIds, state.isHideSeenEnabled) { + derivedStateOf { + val hidden = state.hiddenRepoIds + val needsHideSeen = state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty() + if (hidden.isEmpty() && !needsHideSeen) { + source + } else { + source.filter { repo -> + repo.repository.id !in hidden && + (!needsHideSeen || repo.repository.id !in state.seenRepoIds) } } } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +private fun findRepo( + state: HomeState, + repoId: Long, +): zed.rainxch.core.presentation.model.DiscoveryRepositoryUi? { + fun seq() = sequence { + yieldAll(state.hot) + yieldAll(state.trending) + yieldAll(state.popular) + yieldAll(state.starred) + } + return seq().firstOrNull { it.repository.id == repoId } +} + @Composable -private fun PlatformPopupRow( - label: String, - isSelected: Boolean, - onClick: () -> Unit, -) { +private fun LoadingState() { Box( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 24.dp, vertical = 8.dp), + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = - Arrangement.spacedBy( - 6.dp, - Alignment.Start, - ), - ) { - if (isSelected) { - Icon( - imageVector = Icons.Default.Done, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp), - ) - } - + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(8.dp)) Text( - text = label, - style = MaterialTheme.typography.titleMediumEmphasized, - color = MaterialTheme.colorScheme.onSurface, + text = stringResource(Res.string.home_finding_repositories), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } -// Pins the popup to the window's top-end corner instead of anchoring to its -// parent's `LayoutCoordinates`. The default Compose anchor jitters when the -// parent (here, the platform-icons Row inside the TopAppBar action slot) -// resizes during multi-select toggles, causing the popup to flicker. -private class WindowAnchoredTopEndPopupPositionProvider( - private val topPaddingPx: Int, - private val endPaddingPx: Int, -) : PopupPositionProvider { - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize, - ): IntOffset { - val x = - if (layoutDirection == LayoutDirection.Ltr) { - windowSize.width - popupContentSize.width - endPaddingPx - } else { - endPaddingPx - } - return IntOffset(x.coerceAtLeast(0), topPaddingPx) +@Composable +private fun ErrorState(message: String, onRetry: () -> Unit) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp), + ) { + Text( + text = message, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + GithubStoreButton( + text = stringResource(Res.string.home_retry), + onClick = onRetry, + ) + } } } @@ -766,9 +452,9 @@ private fun Preview() { GithubStoreTheme { HomeScreen( state = HomeState(), - onAction = {}, snackbarHost = SnackbarHostState(), - listState = rememberLazyStaggeredGridState(), + listState = rememberLazyListState(), + onAction = {}, ) } } From de41bc00501c533968abc8b3db8ee86a217904c1 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 12:23:52 +0500 Subject: [PATCH 021/172] feat(home): Discover topbar, CategoryList route, lead pill, accent rings --- .../githubstore/app/di/ViewModelsModule.kt | 6 + .../app/navigation/AppNavigation.kt | 38 +++- .../app/navigation/GithubStoreGraph.kt | 11 ++ .../presentation/vocabulary/FreshnessRing.kt | 4 +- .../zed/rainxch/home/presentation/HomeRoot.kt | 54 +++--- .../categorylist/CategoryListAction.kt | 8 + .../categorylist/CategoryListEvent.kt | 5 + .../categorylist/CategoryListRoot.kt | 173 ++++++++++++++++++ .../categorylist/CategoryListState.kt | 15 ++ .../categorylist/CategoryListViewModel.kt | 115 ++++++++++++ .../presentation/components/HomeTopBar.kt | 43 +---- .../presentation/components/HotCardItem.kt | 74 ++++++-- .../home/presentation/components/LeadCard.kt | 40 +++- 13 files changed, 502 insertions(+), 84 deletions(-) create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListAction.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListEvent.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index 775ab7627..d8cfb2369 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -109,6 +109,12 @@ val viewModelsModule = viewModelOf(::WhatsNewViewModel) viewModelOf(::AnnouncementsViewModel) viewModelOf(::OnboardingViewModel) + viewModel { params -> + zed.rainxch.home.presentation.categorylist.CategoryListViewModel( + category = params.get(), + homeRepository = get(), + ) + } viewModel { MirrorPickerViewModel( mirrorRepository = get(), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 7e38ff76b..c254c9da2 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -115,7 +115,7 @@ fun AppNavigation( ) { composable { HomeRoot( - onNavigateToSearch = { _ -> + onNavigateToSearch = { navController.navigate(GithubStoreGraph.SearchScreen()) }, onNavigateToSettings = { @@ -126,16 +126,40 @@ fun AppNavigation( }, onNavigateToDetails = { repoId -> navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), + GithubStoreGraph.DetailsScreen(repositoryId = repoId), ) }, onNavigateToDeveloperProfile = { username -> navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, + onNavigateToCategoryList = { category -> + navController.navigate( + GithubStoreGraph.CategoryListScreen(category.name), + ) + }, + onNavigateToStarredRepos = { + navController.navigate(GithubStoreGraph.StarredReposScreen) + }, + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val category = + runCatching { + zed.rainxch.home.domain.model.HomeCategory + .valueOf(args.category) + }.getOrDefault( + zed.rainxch.home.domain.model.HomeCategory.HOT_RELEASE, + ) + zed.rainxch.home.presentation.categorylist.CategoryListRoot( + category = category, + onNavigateBack = { navController.navigateUp() }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen(repositoryId = repoId), ) }, ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index e2d079cdd..9ce59dd6e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -81,4 +81,15 @@ sealed interface GithubStoreGraph { @Serializable data object HostTokensScreen : GithubStoreGraph + + /** + * Category-specific list view powering Home's "See all" jumps. `category` is + * a free-form string mapped to a [zed.rainxch.home.domain.model.HomeCategory] + * at the destination (no enum nav arg — same Desktop nav serializer caveat + * as [SearchScreen.initialPlatform]). + */ + @Serializable + data class CategoryListScreen( + val category: String, + ) : GithubStoreGraph } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt index 29ff2b13c..7c4234ab2 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt @@ -25,16 +25,18 @@ fun FreshnessRing( modifier: Modifier = Modifier, sizeDp: Int = 64, strokeDp: Float = if (sizeDp >= 60) 2.5f else 2f, + color: Color? = null, content: @Composable () -> Unit, ) { val f = freshnessOf(daysSinceRelease) + val ringColor = color ?: f.color val ringSize = sizeDp + 14 Box( modifier = modifier.size(ringSize.dp), contentAlignment = Alignment.Center, ) { FreshnessArc( - color = f.color, + color = ringColor, fraction = f.ringFraction, strokeDp = strokeDp, modifier = Modifier.size(ringSize.dp), diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 4bee4da77..d83726485 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -67,10 +67,12 @@ import zed.rainxch.home.presentation.components.TrendingRowItem @Composable fun HomeRoot( onNavigateToSettings: () -> Unit, - onNavigateToSearch: (category: String?) -> Unit, + onNavigateToSearch: () -> Unit, onNavigateToApps: () -> Unit, onNavigateToDetails: (repoId: Long) -> Unit, onNavigateToDeveloperProfile: (username: String) -> Unit, + onNavigateToCategoryList: (zed.rainxch.home.domain.model.HomeCategory) -> Unit, + onNavigateToStarredRepos: () -> Unit, viewModel: HomeViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -95,13 +97,19 @@ fun HomeRoot( listState = listState, onAction = { action -> when (action) { - HomeAction.OnSearchClick -> onNavigateToSearch(null) + HomeAction.OnSearchClick -> onNavigateToSearch() HomeAction.OnSettingsClick -> onNavigateToSettings() HomeAction.OnAppsClick -> onNavigateToApps() - HomeAction.OnSeeAllHot -> onNavigateToSearch("hot") - HomeAction.OnSeeAllTrending -> onNavigateToSearch("trending") - HomeAction.OnSeeAllPopular -> onNavigateToSearch("popular") - HomeAction.OnSeeAllStarred -> onNavigateToSearch("starred") + HomeAction.OnSeeAllHot -> onNavigateToCategoryList( + zed.rainxch.home.domain.model.HomeCategory.HOT_RELEASE, + ) + HomeAction.OnSeeAllTrending -> onNavigateToCategoryList( + zed.rainxch.home.domain.model.HomeCategory.TRENDING, + ) + HomeAction.OnSeeAllPopular -> onNavigateToCategoryList( + zed.rainxch.home.domain.model.HomeCategory.MOST_POPULAR, + ) + HomeAction.OnSeeAllStarred -> onNavigateToStarredRepos() is HomeAction.OnRepoClick -> onNavigateToDetails(action.repo.id) is HomeAction.OnDeveloperClick -> onNavigateToDeveloperProfile(action.username) else -> viewModel.onAction(action) @@ -195,7 +203,14 @@ private fun FeedContent( val visibleStarred by visibleReposState(state, state.starred) val lead = visibleHot.firstOrNull() - val hotTail = visibleHot.drop(1) + // Limit sections on Home to keep the surface tight — full list lives in + // CategoryListScreen via "See all". Pick 6 as a balance between density + // and "skim-and-go" Home feel. + val homeLimit = 6 + val hotTail = visibleHot.drop(1).take(homeLimit) + val trendingPreview = visibleTrending.take(homeLimit) + val popularPreview = visiblePopular.take(homeLimit) + val starredPreview = visibleStarred.take(5) val isScrollbarEnabled = LocalScrollbarEnabled.current ScrollbarContainer( @@ -218,13 +233,6 @@ private fun FeedContent( HomeTopBar( onSearchClick = { onAction(HomeAction.OnSearchClick) }, onSettingsClick = { onAction(HomeAction.OnSettingsClick) }, - actions = { - PlatformFilterAction( - selectedPlatforms = state.selectedPlatforms, - isPlatformPopupVisible = state.isPlatformPopupVisible, - onAction = onAction, - ) - }, ) } @@ -263,16 +271,16 @@ private fun FeedContent( } } - if (visibleTrending.isNotEmpty()) { + if (trendingPreview.isNotEmpty()) { item(key = "trending_header") { SectionHeader( title = "Trending now", - subCount = visibleTrending.size.toString(), + subCount = trendingPreview.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllTrending) }, ) } itemsIndexed( - items = visibleTrending, + items = trendingPreview, key = { _, repo -> "trending_${repo.repository.id}" }, ) { idx, repo -> TrendingRowItem( @@ -284,16 +292,16 @@ private fun FeedContent( } } - if (visiblePopular.isNotEmpty()) { + if (popularPreview.isNotEmpty()) { item(key = "popular_header") { SectionHeader( title = "Most popular", - subCount = visiblePopular.size.toString(), + subCount = popularPreview.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllPopular) }, ) } itemsIndexed( - items = visiblePopular, + items = popularPreview, key = { _, repo -> "popular_${repo.repository.id}" }, ) { idx, repo -> PopularRowItem( @@ -305,15 +313,15 @@ private fun FeedContent( } } - if (state.isUserSignedIn && visibleStarred.isNotEmpty()) { + if (state.isUserSignedIn && starredPreview.isNotEmpty()) { item(key = "starred_header") { SectionHeader( title = "From your stars", - subCount = visibleStarred.size.toString(), + subCount = starredPreview.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllStarred) }, ) } - items(items = visibleStarred, key = { "starred_${it.repository.id}" }) { repo -> + items(items = starredPreview, key = { "starred_${it.repository.id}" }) { repo -> StarredRowItem( repo = repo, onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListAction.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListAction.kt new file mode 100644 index 000000000..bd62ccd0d --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListAction.kt @@ -0,0 +1,8 @@ +package zed.rainxch.home.presentation.categorylist + +sealed interface CategoryListAction { + data object OnNavigateBack : CategoryListAction + data object OnLoadMore : CategoryListAction + data object OnRefresh : CategoryListAction + data class OnRepoClick(val repoId: Long) : CategoryListAction +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListEvent.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListEvent.kt new file mode 100644 index 000000000..5dbcbef4b --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListEvent.kt @@ -0,0 +1,5 @@ +package zed.rainxch.home.presentation.categorylist + +sealed interface CategoryListEvent { + data class NavigateToDetails(val repoId: Long) : CategoryListEvent +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt new file mode 100644 index 000000000..975f82d60 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt @@ -0,0 +1,173 @@ +package zed.rainxch.home.presentation.categorylist + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf +import zed.rainxch.core.presentation.components.buttons.IconButton +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.core.presentation.vocabulary.Squiggle +import zed.rainxch.home.domain.model.HomeCategory +import zed.rainxch.home.presentation.components.PopularRowItem +import zed.rainxch.home.presentation.components.TrendingRowItem + +@Composable +fun CategoryListRoot( + category: HomeCategory, + onNavigateBack: () -> Unit, + onNavigateToDetails: (Long) -> Unit, + viewModel: CategoryListViewModel = koinViewModel { parametersOf(category) }, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + ObserveAsEvents(viewModel.events) { event -> + when (event) { + is CategoryListEvent.NavigateToDetails -> onNavigateToDetails(event.repoId) + } + } + CategoryListScreen( + state = state, + onAction = viewModel::onAction, + onBack = onNavigateBack, + ) +} + +@Composable +fun CategoryListScreen( + state: CategoryListState, + onAction: (CategoryListAction) -> Unit, + onBack: () -> Unit, +) { + val listState = rememberLazyListState() + val shouldLoadMore by remember { + derivedStateOf { + val total = listState.layoutInfo.totalItemsCount + val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + total > 0 && lastVisible >= total - 4 + } + } + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore && !state.isLoadingMore && state.hasMorePages) { + onAction(CategoryListAction.OnLoadMore) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .systemBarsPadding(), + ) { + CategoryListTopBar(state.category, onBack) + if (state.isLoading && state.repos.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + return@Column + } + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + items(items = state.repos, key = { it.repository.id }) { repo -> + val rank = state.repos.indexOf(repo) + 1 + when (state.category) { + HomeCategory.MOST_POPULAR -> PopularRowItem( + rank = rank, + repo = repo, + onClick = { onAction(CategoryListAction.OnRepoClick(repo.repository.id)) }, + onLongClick = { }, + ) + else -> TrendingRowItem( + rank = rank, + repo = repo, + onClick = { onAction(CategoryListAction.OnRepoClick(repo.repository.id)) }, + onLongClick = { }, + ) + } + } + if (state.isLoadingMore) { + item { + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } + } + } + } + } +} + +@Composable +private fun CategoryListTopBar(category: HomeCategory, onBack: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + Column(modifier = Modifier.padding(start = 4.dp)) { + Text( + text = categoryTitle(category), + style = MaterialTheme.typography.headlineSmall.copy( + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.SemiBold, + fontSize = 26.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.size(4.dp)) + Squiggle() + } + } +} + +private fun categoryTitle(category: HomeCategory): String = when (category) { + HomeCategory.HOT_RELEASE -> "Hot releases" + HomeCategory.TRENDING -> "Trending now" + HomeCategory.MOST_POPULAR -> "Most popular" +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt new file mode 100644 index 000000000..2967c6a71 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt @@ -0,0 +1,15 @@ +package zed.rainxch.home.presentation.categorylist + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.home.domain.model.HomeCategory + +data class CategoryListState( + val category: HomeCategory = HomeCategory.HOT_RELEASE, + val repos: ImmutableList = persistentListOf(), + val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMorePages: Boolean = true, + val errorMessage: String? = null, +) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt new file mode 100644 index 000000000..473ed8303 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt @@ -0,0 +1,115 @@ +package zed.rainxch.home.presentation.categorylist + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import zed.rainxch.core.presentation.utils.toUi +import zed.rainxch.home.domain.model.HomeCategory +import zed.rainxch.home.domain.repository.HomeRepository + +class CategoryListViewModel( + private val category: HomeCategory, + private val homeRepository: HomeRepository, +) : ViewModel() { + + private val _state = MutableStateFlow(CategoryListState(category = category)) + val state = _state.asStateFlow() + + private val _events = Channel() + val events = _events.receiveAsFlow() + + private var nextPage = 1 + + init { + loadPage(initial = true) + } + + fun onAction(action: CategoryListAction) { + when (action) { + CategoryListAction.OnLoadMore -> + if (!_state.value.isLoadingMore && _state.value.hasMorePages) loadPage(initial = false) + CategoryListAction.OnRefresh -> { + nextPage = 1 + _state.update { + it.copy( + repos = persistentListOf(), + hasMorePages = true, + errorMessage = null, + ) + } + loadPage(initial = true) + } + is CategoryListAction.OnRepoClick -> viewModelScope.launch { + _events.send(CategoryListEvent.NavigateToDetails(action.repoId)) + } + CategoryListAction.OnNavigateBack -> Unit + } + } + + private fun loadPage(initial: Boolean) { + viewModelScope.launch { + _state.update { + if (initial) it.copy(isLoading = true) else it.copy(isLoadingMore = true) + } + val flow = when (category) { + HomeCategory.HOT_RELEASE -> homeRepository.getHotReleaseRepositories(emptySet(), nextPage) + HomeCategory.TRENDING -> homeRepository.getTrendingRepositories(emptySet(), nextPage) + HomeCategory.MOST_POPULAR -> homeRepository.getMostPopular(emptySet(), nextPage) + } + runCatching { flow.firstOrNull() } + .onSuccess { paginated -> + if (paginated == null) { + _state.update { + it.copy( + isLoading = false, + isLoadingMore = false, + hasMorePages = false, + ) + } + return@onSuccess + } + val existing: List = _state.value.repos + val incoming = paginated.repos.map { repo -> + zed.rainxch.core.presentation.model.DiscoveryRepositoryUi( + isInstalled = false, + isUpdateAvailable = false, + isFavourite = false, + isStarred = false, + isSeen = false, + isCurrentUserOwner = false, + repository = repo.toUi(), + ) + } + val seenIds = existing.map { it.repository.id }.toHashSet() + val merged = (existing + incoming.filter { it.repository.id !in seenIds }).toImmutableList() + nextPage += 1 + _state.update { + it.copy( + repos = merged, + isLoading = false, + isLoadingMore = false, + hasMorePages = incoming.isNotEmpty(), + errorMessage = null, + ) + } + } + .onFailure { e -> + _state.update { + it.copy( + isLoading = false, + isLoadingMore = false, + errorMessage = e.message ?: "Failed to load", + ) + } + } + } + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt index b095b985f..f45134b80 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt @@ -1,12 +1,9 @@ package zed.rainxch.home.presentation.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings @@ -16,23 +13,22 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.buttons.IconButton -import zed.rainxch.core.presentation.vocabulary.CookieShape -import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.app_name +/** + * Home top bar — "Discover" title + Search + Settings actions. Platform filter + * lives in Tweaks → Discovery (per maintainer call, P12). Cookie brand mark moved + * to the Desktop drawer; the bottom nav carries the Cookie identity on Android. + */ @Composable fun HomeTopBar( onSearchClick: () -> Unit, onSettingsClick: () -> Unit, modifier: Modifier = Modifier, - actions: @Composable (RowScope.() -> Unit) = {}, ) { Row( modifier = modifier @@ -41,18 +37,16 @@ fun HomeTopBar( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - CookieMark() Text( - text = stringResource(Res.string.app_name), - style = MaterialTheme.typography.titleLarge.copy( + text = "Discover", + style = MaterialTheme.typography.displaySmall.copy( fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, + fontSize = 28.sp, ), color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), ) - actions() IconButton(onClick = onSearchClick) { Icon( imageVector = Icons.Outlined.Search, @@ -69,24 +63,3 @@ fun HomeTopBar( } } } - -@Composable -private fun CookieMark() { - androidx.compose.foundation.layout.Box( - modifier = Modifier - .size(32.dp) - .clip(CookieShape) - .background(MaterialTheme.colorScheme.primary), - contentAlignment = Alignment.Center, - ) { - Text( - text = "G", - style = MaterialTheme.typography.titleMedium.copy( - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - ), - color = MaterialTheme.colorScheme.onPrimary, - ) - } -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt index ee3919a3c..9694d8190 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -1,7 +1,9 @@ package zed.rainxch.home.presentation.components import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,9 +11,11 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -23,15 +27,18 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.cards.CompactCard import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.core.presentation.vocabulary.AppAccentResolver import zed.rainxch.core.presentation.vocabulary.FreshnessRing import zed.rainxch.core.presentation.vocabulary.PlatformGlyph import zed.rainxch.core.presentation.vocabulary.PlatformKind import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.core.presentation.vocabulary.TopicGlyph +import zed.rainxch.core.presentation.vocabulary.freshnessOf @OptIn(ExperimentalFoundationApi::class) @Composable @@ -43,22 +50,34 @@ fun HotCardItem( ) { val r = repo.repository val days = daysSinceIso(r.updatedAt) + val isDark = isSystemInDarkTheme() + val accent = AppAccentResolver.resolve( + backendHex = null, + topics = r.topics.orEmpty(), + primaryLanguage = r.language, + ) + val freshness = freshnessOf(days) Box( modifier = modifier - .width(280.dp) + .width(260.dp) + .height(220.dp) .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { CompactCard { Row( - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth(), ) { - FreshnessRing(daysSinceRelease = days, sizeDp = 48) { + FreshnessRing( + daysSinceRelease = days, + sizeDp = 44, + color = accent.c, + ) { GitHubStoreImage( imageModel = { r.owner.avatarUrl }, - modifier = Modifier.size(48.dp).clip(CircleShape), + modifier = Modifier.size(44.dp).clip(CircleShape), ) } Column(modifier = Modifier.weight(1f)) { @@ -72,20 +91,21 @@ fun HotCardItem( maxLines = 1, overflow = TextOverflow.Ellipsis, ) + Spacer(Modifier.height(2.dp)) StarTier(stars = r.stargazersCount) } + DaysAgoPill(days = days, color = freshness.color) } Spacer(Modifier.height(8.dp)) - if (!r.description.isNullOrBlank()) { - Text( - text = r.description!!, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Spacer(Modifier.height(10.dp)) - } + Text( + text = r.description.orEmpty(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.height(36.dp), + ) + Spacer(Modifier.height(10.dp)) HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant, thickness = 0.5.dp, @@ -113,4 +133,30 @@ fun HotCardItem( } } } + // Suppress unused dark-mode for now; reserved for future accent-bloom tweak. + @Suppress("UNUSED_EXPRESSION") isDark +} + +@Composable +private fun DaysAgoPill(days: Int, color: androidx.compose.ui.graphics.Color) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(color.copy(alpha = 0.18f)) + .padding(horizontal = 8.dp, vertical = 3.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.size(5.dp).clip(CircleShape).background(color), + ) + Text( + text = "${days}d", + color = color, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + ) + } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 8fb1cfe93..45f039722 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -1,6 +1,7 @@ package zed.rainxch.home.presentation.components import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -10,8 +11,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,9 +32,9 @@ import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.components.cards.LeadHeroCard import zed.rainxch.core.presentation.vocabulary.AppAccentResolver import zed.rainxch.core.presentation.vocabulary.FreshnessRing -import zed.rainxch.core.presentation.vocabulary.Heartbeat import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.core.presentation.vocabulary.TopicGlyph +import zed.rainxch.core.presentation.vocabulary.freshnessOf @OptIn(ExperimentalFoundationApi::class) @Composable @@ -50,18 +53,20 @@ fun LeadCard( ) val days = daysSinceIso(r.updatedAt) - Box( + Column( modifier = modifier .fillMaxWidth() .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { + HotPill(days = days) + Spacer(Modifier.height(8.dp)) LeadHeroCard(accent = accent, isDark = isDark) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth(), ) { - FreshnessRing(daysSinceRelease = days, sizeDp = 80) { + FreshnessRing(daysSinceRelease = days, sizeDp = 80, color = accent.c) { GitHubStoreImage( imageModel = { r.owner.avatarUrl }, modifier = Modifier.size(80.dp).clip(CircleShape), @@ -93,7 +98,6 @@ fun LeadCard( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { StarTier(stars = r.stargazersCount) - Heartbeat(daysSinceCommit = days) r.topics.orEmpty().take(3).forEach { topic -> TopicGlyph(topic = topic, sizeDp = 14) } @@ -128,3 +132,31 @@ internal fun ctaLabel(repo: DiscoveryRepositoryUi): String = when { repo.isInstalled -> "Open" else -> "Get" } + +@Composable +private fun HotPill(days: Int) { + val freshness = zed.rainxch.core.presentation.vocabulary.freshnessOf(days) + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(freshness.color.copy(alpha = 0.18f)) + .padding(horizontal = 12.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background(freshness.color), + ) + Text( + text = "HOT · ${days}d ago", + color = freshness.color, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + ) + } +} From a0c72fa64d94f3fa28eddaaddb5782fa6b06d567 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 12:43:44 +0500 Subject: [PATCH 022/172] fix(home): sub-day relative time labels in card pills --- .../components/HomeTimeHelpers.kt | 33 +++++++++++++++++++ .../presentation/components/HotCardItem.kt | 6 ++-- .../home/presentation/components/LeadCard.kt | 8 ++--- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt index f63939208..72efcb7c0 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt @@ -14,3 +14,36 @@ internal fun daysSinceIso(isoInstant: String): Int { if (diffMs <= 0L) return 0 return (diffMs / 86_400_000L).toInt() } + +/** + * Sub-day-aware relative label. Falls back to "Nd" once gap ≥ 24h. Inputs: + * - <1m → "now" + * - <1h → "Nm" + * - <24h → "Nh" + * - <30d → "Nd" + * - <12mo → "Nmo" + * - else → "Ny" + * + * Used by the Hot card corner pill + Lead card "HOT · X ago" so a fresh release + * 4 hours old shows "4h" instead of "0d". + */ +@OptIn(ExperimentalTime::class) +internal fun relativeAgo(isoInstant: String): String { + val trimmed = isoInstant.trim() + if (trimmed.isEmpty()) return "" + val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return "" + val nowMs = Clock.System.now().toEpochMilliseconds() + val diffMs = nowMs - parsed.toEpochMilliseconds() + if (diffMs <= 0L) return "now" + val minutes = diffMs / 60_000L + if (minutes < 1L) return "now" + if (minutes < 60L) return "${minutes}m" + val hours = minutes / 60L + if (hours < 24L) return "${hours}h" + val days = hours / 24L + if (days < 30L) return "${days}d" + val months = days / 30L + if (months < 12L) return "${months}mo" + val years = days / 365L + return "${years}y" +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt index 9694d8190..e40f539c0 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -94,7 +94,7 @@ fun HotCardItem( Spacer(Modifier.height(2.dp)) StarTier(stars = r.stargazersCount) } - DaysAgoPill(days = days, color = freshness.color) + DaysAgoPill(label = relativeAgo(r.updatedAt), color = freshness.color) } Spacer(Modifier.height(8.dp)) Text( @@ -138,7 +138,7 @@ fun HotCardItem( } @Composable -private fun DaysAgoPill(days: Int, color: androidx.compose.ui.graphics.Color) { +private fun DaysAgoPill(label: String, color: androidx.compose.ui.graphics.Color) { Row( modifier = Modifier .clip(RoundedCornerShape(50)) @@ -151,7 +151,7 @@ private fun DaysAgoPill(days: Int, color: androidx.compose.ui.graphics.Color) { modifier = Modifier.size(5.dp).clip(CircleShape).background(color), ) Text( - text = "${days}d", + text = label, color = color, style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.SemiBold, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 45f039722..6cdb7c36f 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -58,7 +58,7 @@ fun LeadCard( .fillMaxWidth() .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { - HotPill(days = days) + HotPill(days = days, ago = relativeAgo(r.updatedAt)) Spacer(Modifier.height(8.dp)) LeadHeroCard(accent = accent, isDark = isDark) { Row( @@ -134,8 +134,8 @@ internal fun ctaLabel(repo: DiscoveryRepositoryUi): String = when { } @Composable -private fun HotPill(days: Int) { - val freshness = zed.rainxch.core.presentation.vocabulary.freshnessOf(days) +private fun HotPill(days: Int, ago: String) { + val freshness = freshnessOf(days) Row( modifier = Modifier .clip(RoundedCornerShape(50)) @@ -151,7 +151,7 @@ private fun HotPill(days: Int) { .background(freshness.color), ) Text( - text = "HOT · ${days}d ago", + text = "HOT · $ago ago", color = freshness.color, style = MaterialTheme.typography.labelSmall.copy( fontWeight = FontWeight.SemiBold, From 9fa83f01ff3436e07af308b92d9b39b027e4e227 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 14:43:34 +0500 Subject: [PATCH 023/172] feat(vocabulary): swap to 14 canonical topic codes + new glyphs --- .../core/presentation/theme/tokens/Tokens.kt | 22 +- .../presentation/vocabulary/TopicGlyph.kt | 274 ++++++++++++------ 2 files changed, 191 insertions(+), 105 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt index ca5d25d3f..197cf3fab 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt @@ -289,21 +289,19 @@ object Tokens { } /** - * Topic glyphs supported by [TopicGlyph]. Aliases map non-canonical topic strings - * to a supported glyph (e.g. "password-manager" → "key"). + * Canonical topic codes (14) emitted by the backend topic mapper. Frontend + * draws one glyph per code. Backend normalizes raw GitHub topics into this + * set — frontend does NOT keep an alias table anymore. + * + * If backend ships a code we don't have a glyph for yet, [TopicGlyph] silently + * renders nothing (graceful degrade until next frontend release). */ object Topics { val supported = setOf( - "self-hosted", "mobile", "photo", "video", "book", "manga", - "key", "audio", "backup", "reader", "cross-platform", "cloud", - ) - val aliases = mapOf( - "password-manager" to "key", - "podcast" to "audio", - "ebook" to "book", - "messaging" to "key", - "vpn" to "cloud", - "note" to "book", + "security", "networking", "ai", "notes", + "audio", "video", "photo", "reader", + "messaging", "browser", "self-hosted", "backup", + "social", "launcher", ) } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt index f4233b8f0..df6dc12fe 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt @@ -18,9 +18,10 @@ import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Tokens /** - * Micro-pictogram per supported topic (DESIGN.md §4.2). Monochrome — never carries - * an accent. Returns `null` (renders nothing) when the topic isn't in the supported - * set or its alias map. + * Micro-pictogram per canonical topic code (DESIGN.md §4.2). Backend now emits a + * normalized topic-code set per repo (R12-v2); aliases are no longer resolved + * here. Monochrome — never carries the per-app accent. Renders nothing when + * [topic] isn't in the supported set ([Tokens.Topics.supported]). */ @Composable fun TopicGlyph( @@ -29,62 +30,208 @@ fun TopicGlyph( sizeDp: Int = 14, color: Color = LocalContentColor.current, ) { - val resolved = resolveTopic(topic) ?: return + val key = topic.lowercase() + if (key !in Tokens.Topics.supported) return Canvas(modifier = modifier.size(sizeDp.dp)) { val sw = 1.7f.dp.toPx() val stroke = Stroke(width = sw, cap = StrokeCap.Round, join = StrokeJoin.Round) - when (resolved) { - "self-hosted" -> drawSelfHosted(color, stroke) - "mobile" -> drawMobile(color, stroke) - "photo" -> drawPhoto(color, stroke) - "video" -> drawVideo(color, stroke) - "book" -> drawBook(color, stroke) - "manga" -> drawManga(color, stroke) - "key" -> drawKey(color, stroke) + when (key) { + "security" -> drawSecurity(color, stroke) + "networking" -> drawNetworking(color, stroke) + "ai" -> drawAi(color, stroke) + "notes" -> drawNotes(color, stroke) "audio" -> drawAudio(color) - "backup" -> drawBackup(color, stroke) + "video" -> drawVideo(color, stroke) + "photo" -> drawPhoto(color, stroke) "reader" -> drawReader(color, stroke) - "cross-platform" -> drawCrossPlatform(color, stroke) - "cloud" -> drawCloud(color, stroke) + "messaging" -> drawMessaging(color, stroke) + "browser" -> drawBrowser(color, stroke) + "self-hosted" -> drawSelfHosted(color, stroke) + "backup" -> drawBackup(color, stroke) + "social" -> drawSocial(color, stroke) + "launcher" -> drawLauncher(color) } } } -private fun resolveTopic(topic: String): String? { - val key = topic.lowercase() - if (key in Tokens.Topics.supported) return key - return Tokens.Topics.aliases[key] +private fun DrawScope.scaled(viewBoxValue: Float) = viewBoxValue / 24f * size.minDimension + +// ───────────────────────────────────────────────────────────────────────────── +// New glyphs (7) — canonical R12-v2 topic codes +// ───────────────────────────────────────────────────────────────────────────── + +private fun DrawScope.drawSecurity(c: Color, s: Stroke) { + // Padlock — shackle arc on top, body below. + val shackle = Path().apply { + moveTo(scaled(8f), scaled(11f)) + lineTo(scaled(8f), scaled(8f)) + cubicTo(scaled(8f), scaled(4f), scaled(16f), scaled(4f), scaled(16f), scaled(8f)) + lineTo(scaled(16f), scaled(11f)) + } + drawPath(shackle, c, style = s) + drawRoundRect( + color = c, + topLeft = Offset(scaled(5.5f), scaled(11f)), + size = Size(scaled(13f), scaled(9f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), + style = s, + ) + // Keyhole dot + drawCircle(color = c, radius = scaled(1.3f), center = Offset(scaled(12f), scaled(15f))) } -private fun DrawScope.scaled(viewBoxValue: Float) = viewBoxValue / 24f * size.minDimension +private fun DrawScope.drawNetworking(c: Color, s: Stroke) { + // Three ascending WiFi-style arcs + dot + drawArc( + color = c, + startAngle = 215f, + sweepAngle = 110f, + useCenter = false, + topLeft = Offset(scaled(3f), scaled(7f)), + size = Size(scaled(18f), scaled(18f)), + style = s, + ) + drawArc( + color = c, + startAngle = 215f, + sweepAngle = 110f, + useCenter = false, + topLeft = Offset(scaled(6f), scaled(10f)), + size = Size(scaled(12f), scaled(12f)), + style = s, + ) + drawArc( + color = c, + startAngle = 215f, + sweepAngle = 110f, + useCenter = false, + topLeft = Offset(scaled(9f), scaled(13f)), + size = Size(scaled(6f), scaled(6f)), + style = s, + ) + drawCircle(color = c, radius = scaled(1f), center = Offset(scaled(12f), scaled(19f))) +} -private fun DrawScope.drawSelfHosted(c: Color, s: Stroke) { - val p = Path().apply { - moveTo(scaled(4f), scaled(12f)) - lineTo(scaled(12f), scaled(5f)) - lineTo(scaled(20f), scaled(12f)) - lineTo(scaled(20f), scaled(19f)) - lineTo(scaled(4f), scaled(19f)) +private fun DrawScope.drawAi(c: Color, s: Stroke) { + // 4-point spark (sparkle) + small companion dot + val spark = Path().apply { + moveTo(scaled(12f), scaled(3f)) + lineTo(scaled(14f), scaled(10f)) + lineTo(scaled(21f), scaled(12f)) + lineTo(scaled(14f), scaled(14f)) + lineTo(scaled(12f), scaled(21f)) + lineTo(scaled(10f), scaled(14f)) + lineTo(scaled(3f), scaled(12f)) + lineTo(scaled(10f), scaled(10f)) close() } - drawPath(p, c, style = s) + drawPath(spark, c, style = s) } -private fun DrawScope.drawMobile(c: Color, s: Stroke) { +private fun DrawScope.drawNotes(c: Color, s: Stroke) { + // Pencil over paper — paper rect + diagonal pencil drawRoundRect( color = c, - topLeft = Offset(scaled(7f), scaled(3f)), - size = Size(scaled(10f), scaled(18f)), - cornerRadius = CornerRadius(scaled(2f), scaled(2f)), + topLeft = Offset(scaled(4f), scaled(5f)), + size = Size(scaled(12f), scaled(16f)), + cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), style = s, ) + val pencil = Path().apply { + moveTo(scaled(15f), scaled(8f)) + lineTo(scaled(21f), scaled(2f)) + lineTo(scaled(22.5f), scaled(3.5f)) + lineTo(scaled(16.5f), scaled(9.5f)) + close() + } + drawPath(pencil, c, style = s) drawLine( color = c, - start = Offset(scaled(11f), scaled(18f)), - end = Offset(scaled(13f), scaled(18f)), + start = Offset(scaled(7f), scaled(11f)), + end = Offset(scaled(13f), scaled(11f)), strokeWidth = s.width, cap = StrokeCap.Round, ) + drawLine( + color = c, + start = Offset(scaled(7f), scaled(15f)), + end = Offset(scaled(13f), scaled(15f)), + strokeWidth = s.width, + cap = StrokeCap.Round, + ) +} + +private fun DrawScope.drawMessaging(c: Color, s: Stroke) { + // Speech bubble — rounded rect + tail + drawRoundRect( + color = c, + topLeft = Offset(scaled(3f), scaled(4f)), + size = Size(scaled(18f), scaled(13f)), + cornerRadius = CornerRadius(scaled(2.5f), scaled(2.5f)), + style = s, + ) + val tail = Path().apply { + moveTo(scaled(7f), scaled(17f)) + lineTo(scaled(6f), scaled(21f)) + lineTo(scaled(11f), scaled(17f)) + close() + } + drawPath(tail, c, style = s) +} + +private fun DrawScope.drawBrowser(c: Color, s: Stroke) { + // Compass — circle + needle (pointer) + drawCircle(color = c, radius = scaled(8f), center = Offset(scaled(12f), scaled(12f)), style = s) + val needle = Path().apply { + moveTo(scaled(12f), scaled(6f)) + lineTo(scaled(14.5f), scaled(13f)) + lineTo(scaled(12f), scaled(11.5f)) + lineTo(scaled(9.5f), scaled(13f)) + close() + } + drawPath(needle, c) +} + +private fun DrawScope.drawSocial(c: Color, s: Stroke) { + // Two people — circles + shoulder arcs + drawCircle(color = c, radius = scaled(2.4f), center = Offset(scaled(9f), scaled(8f)), style = s) + drawCircle(color = c, radius = scaled(2.4f), center = Offset(scaled(16f), scaled(9f)), style = s) + val shoulderA = Path().apply { + moveTo(scaled(4.5f), scaled(20f)) + cubicTo(scaled(4.5f), scaled(14f), scaled(13.5f), scaled(14f), scaled(13.5f), scaled(20f)) + } + drawPath(shoulderA, c, style = s) + val shoulderB = Path().apply { + moveTo(scaled(11.5f), scaled(20f)) + cubicTo(scaled(11.5f), scaled(15.5f), scaled(20.5f), scaled(15.5f), scaled(20.5f), scaled(20f)) + } + drawPath(shoulderB, c, style = s) +} + +private fun DrawScope.drawLauncher(c: Color) { + // 3×3 grid dots + val r = scaled(1.5f) + listOf(7f, 12f, 17f).forEach { cx -> + listOf(7f, 12f, 17f).forEach { cy -> + drawCircle(color = c, radius = r, center = Offset(scaled(cx), scaled(cy))) + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Existing glyphs (6) — kept verbatim +// ───────────────────────────────────────────────────────────────────────────── + +private fun DrawScope.drawSelfHosted(c: Color, s: Stroke) { + val p = Path().apply { + moveTo(scaled(4f), scaled(12f)) + lineTo(scaled(12f), scaled(5f)) + lineTo(scaled(20f), scaled(12f)) + lineTo(scaled(20f), scaled(19f)) + lineTo(scaled(4f), scaled(19f)) + close() + } + drawPath(p, c, style = s) } private fun DrawScope.drawPhoto(c: Color, s: Stroke) { @@ -122,38 +269,6 @@ private fun DrawScope.drawVideo(c: Color, s: Stroke) { drawPath(triangle, c) } -private fun DrawScope.drawBook(c: Color, s: Stroke) { - val p = Path().apply { - moveTo(scaled(4f), scaled(5f)) - lineTo(scaled(12f), scaled(7f)) - lineTo(scaled(20f), scaled(5f)) - lineTo(scaled(20f), scaled(19f)) - lineTo(scaled(12f), scaled(21f)) - lineTo(scaled(4f), scaled(19f)) - close() - moveTo(scaled(12f), scaled(7f)) - lineTo(scaled(12f), scaled(21f)) - } - drawPath(p, c, style = s) -} - -private fun DrawScope.drawManga(c: Color, s: Stroke) { - drawRect(color = c, topLeft = Offset(scaled(4f), scaled(4f)), size = Size(scaled(7f), scaled(16f)), style = s) - drawRect(color = c, topLeft = Offset(scaled(13f), scaled(4f)), size = Size(scaled(7f), scaled(16f)), style = s) -} - -private fun DrawScope.drawKey(c: Color, s: Stroke) { - drawCircle(color = c, radius = scaled(3.2f), center = Offset(scaled(8f), scaled(12f)), style = s) - val teeth = Path().apply { - moveTo(scaled(11.2f), scaled(12f)) - lineTo(scaled(20f), scaled(12f)) - lineTo(scaled(20f), scaled(16f)) - moveTo(scaled(17f), scaled(12f)) - lineTo(scaled(17f), scaled(14.5f)) - } - drawPath(teeth, c, style = s) -} - private fun DrawScope.drawAudio(c: Color) { drawRect(color = c, topLeft = Offset(scaled(4f), scaled(10f)), size = Size(scaled(2.5f), scaled(8f))) drawRect(color = c, topLeft = Offset(scaled(10.75f), scaled(6f)), size = Size(scaled(2.5f), scaled(14f))) @@ -189,30 +304,3 @@ private fun DrawScope.drawReader(c: Color, s: Stroke) { } drawPath(p, c, style = s) } - -private fun DrawScope.drawCrossPlatform(c: Color, s: Stroke) { - drawRoundRect( - color = c, - topLeft = Offset(scaled(3f), scaled(3f)), - size = Size(scaled(11f), scaled(11f)), - cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), - style = s, - ) - drawRoundRect( - color = c, - topLeft = Offset(scaled(10f), scaled(10f)), - size = Size(scaled(11f), scaled(11f)), - cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), - ) -} - -private fun DrawScope.drawCloud(c: Color, s: Stroke) { - val p = Path().apply { - moveTo(scaled(7f), scaled(17f)) - cubicTo(scaled(3f), scaled(17f), scaled(3f), scaled(9f), scaled(7f), scaled(9f)) - cubicTo(scaled(7f), scaled(4f), scaled(17f), scaled(4f), scaled(17f), scaled(10f)) - cubicTo(scaled(20.5f), scaled(10f), scaled(20.5f), scaled(17f), scaled(17f), scaled(17f)) - close() - } - drawPath(p, c, style = s) -} From 6d7810f70939a0fb28802fac0f446a7269c82a52 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 14:57:11 +0500 Subject: [PATCH 024/172] feat(vocabulary): add privacy topic glyph (eye + strikethrough) --- .../core/presentation/theme/tokens/Tokens.kt | 2 +- .../presentation/vocabulary/TopicGlyph.kt | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt index 197cf3fab..480ae0f43 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt @@ -298,7 +298,7 @@ object Tokens { */ object Topics { val supported = setOf( - "security", "networking", "ai", "notes", + "security", "privacy", "networking", "ai", "notes", "audio", "video", "photo", "reader", "messaging", "browser", "self-hosted", "backup", "social", "launcher", diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt index df6dc12fe..e6b132487 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt @@ -37,6 +37,7 @@ fun TopicGlyph( val stroke = Stroke(width = sw, cap = StrokeCap.Round, join = StrokeJoin.Round) when (key) { "security" -> drawSecurity(color, stroke) + "privacy" -> drawPrivacy(color, stroke) "networking" -> drawNetworking(color, stroke) "ai" -> drawAi(color, stroke) "notes" -> drawNotes(color, stroke) @@ -80,6 +81,25 @@ private fun DrawScope.drawSecurity(c: Color, s: Stroke) { drawCircle(color = c, radius = scaled(1.3f), center = Offset(scaled(12f), scaled(15f))) } +private fun DrawScope.drawPrivacy(c: Color, s: Stroke) { + // Eye outline (almond) + pupil + diagonal strikethrough. + val eye = Path().apply { + moveTo(scaled(3f), scaled(12f)) + quadraticTo(scaled(12f), scaled(4.5f), scaled(21f), scaled(12f)) + quadraticTo(scaled(12f), scaled(19.5f), scaled(3f), scaled(12f)) + close() + } + drawPath(eye, c, style = s) + drawCircle(color = c, radius = scaled(2.2f), center = Offset(scaled(12f), scaled(12f))) + drawLine( + color = c, + start = Offset(scaled(4f), scaled(20f)), + end = Offset(scaled(20f), scaled(4f)), + strokeWidth = s.width * 1.4f, + cap = StrokeCap.Round, + ) +} + private fun DrawScope.drawNetworking(c: Color, s: Stroke) { // Three ascending WiFi-style arcs + dot drawArc( From 5088ad851a8ccb0a1542c4ff8cd5d722c8da53c3 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 15:33:29 +0500 Subject: [PATCH 025/172] fix(home): fetch full repo data for starred section via DetailsRepository --- .../kotlin/zed/rainxch/githubstore/Main.kt | 17 +-- .../zed/rainxch/githubstore/MainState.kt | 1 - .../zed/rainxch/githubstore/MainViewModel.kt | 7 - .../githubstore/app/di/ViewModelsModule.kt | 2 - .../app/navigation/AppNavigation.kt | 63 ++++----- .../app/navigation/GithubStoreGraph.kt | 14 -- .../domain/repository/TweaksRepository.kt | 7 - .../components/section/SectionHeader.kt | 7 +- .../presentation/import/ExternalImportRoot.kt | 2 + feature/home/presentation/build.gradle.kts | 1 + .../rainxch/home/presentation/HomeAction.kt | 10 -- .../zed/rainxch/home/presentation/HomeRoot.kt | 64 +-------- .../home/presentation/HomeViewModel.kt | 123 +++++------------- .../presentation/components/HomeTopBar.kt | 26 ---- .../mirror/AutoSuggestMirrorViewModel.kt | 47 ------- .../components/AutoSuggestMirrorSheet.kt | 75 ----------- 16 files changed, 76 insertions(+), 390 deletions(-) delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/AutoSuggestMirrorViewModel.kt delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/AutoSuggestMirrorSheet.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt index 410e9386d..242d333ed 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import coil3.ImageLoader +import coil3.compose.setSingletonImageLoaderFactory import kotlinx.coroutines.delay import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.auth.presentation.AuthDeepLinkBus @@ -33,11 +35,8 @@ import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel @Composable fun App(deepLinkUri: String? = null) { - // Wire Coil's singleton ImageLoader with the SVG decoder so README - // images that point to .svg URLs (shields.io badges, diagrams, - // hero images) render natively instead of failing silently. - coil3.compose.setSingletonImageLoaderFactory { context -> - coil3.ImageLoader + setSingletonImageLoaderFactory { context -> + ImageLoader .Builder(context) .components { add(coil3.svg.SvgDecoder.Factory()) } .build() @@ -49,9 +48,6 @@ fun App(deepLinkUri: String? = null) { val navController = rememberNavController() val currentScreen = navController.currentBackStackEntryAsState().value.getCurrentScreen() - // First-launch redirect to Onboarding (D17 / D6.5). Fires once when the - // persistence layer reports `onboardingComplete = false`. `null` means the - // flow hasn't emitted yet — wait, don't redirect into a flash of Home. LaunchedEffect(state.onboardingComplete) { if (state.onboardingComplete == false && currentScreen !is GithubStoreGraph.OnboardingScreen @@ -75,9 +71,6 @@ fun App(deepLinkUri: String? = null) { } DeepLinkDestination.Apps -> { - // Pending-install notification dropped us here. - // Navigate to the apps tab so the user can finish - // the deferred install from the row. navController.navigate(GithubStoreGraph.AppsScreen) { popUpTo(GithubStoreGraph.HomeScreen) { saveState = true @@ -182,8 +175,6 @@ fun App(deepLinkUri: String? = null) { AppNavigation( navController = navController, - isScrollbarEnabled = state.isScrollbarEnabled, - contentWidth = state.contentWidth, ) val whatsNewViewModel: WhatsNewViewModel = koinViewModel() diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt index f9bbb61a2..b16fbdab7 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainState.kt @@ -16,6 +16,5 @@ data class MainState( val currentFontTheme: FontTheme = FontTheme.CUSTOM, val isScrollbarEnabled: Boolean = false, val contentWidth: ContentWidth = ContentWidth.COMPACT, - /** First-launch onboarding state. `null` while the flow is still loading. */ val onboardingComplete: Boolean? = null, ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt index f45fb92b3..6ba54c78c 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt @@ -76,13 +76,6 @@ class MainViewModel( } viewModelScope.launch { - // One-shot read of the persisted onboarding flag. Collecting the - // full flow causes a race on Desktop (and any platform whose KSafe - // backend emits the `false` default before the persisted `true` - // lands): the App.kt redirect fires on the default emission and - // pushes the user into onboarding every launch. `.first()` blocks - // until the gated flow yields the actual disk value, then we - // update state exactly once. val complete = tweaksRepository.getOnboardingComplete().first() _state.update { it.copy(onboardingComplete = complete) } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index d8cfb2369..c62ac7570 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -23,7 +23,6 @@ import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.feedback.FeedbackViewModel import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesViewModel import zed.rainxch.tweaks.presentation.hosttokens.HostTokensViewModel -import zed.rainxch.tweaks.presentation.mirror.AutoSuggestMirrorViewModel import zed.rainxch.tweaks.presentation.mirror.MirrorPickerViewModel import zed.rainxch.tweaks.presentation.skipped.SkippedUpdatesViewModel @@ -102,7 +101,6 @@ val viewModelsModule = viewModelOf(::FeedbackViewModel) viewModelOf(::StarredReposViewModel) viewModelOf(::StarredPickerViewModel) - viewModelOf(::AutoSuggestMirrorViewModel) viewModelOf(::SkippedUpdatesViewModel) viewModelOf(::HiddenRepositoriesViewModel) viewModelOf(::HostTokensViewModel) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index c254c9da2..5e55bbeb9 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -30,6 +30,7 @@ import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf import zed.rainxch.apps.presentation.AppsRoot import zed.rainxch.apps.presentation.AppsViewModel +import zed.rainxch.apps.presentation.import.EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY import zed.rainxch.apps.presentation.import.ExternalImportRoot import zed.rainxch.auth.presentation.AuthenticationRoot import zed.rainxch.core.domain.getPlatform @@ -54,15 +55,9 @@ import zed.rainxch.starred.presentation.StarredReposRoot import zed.rainxch.tweaks.presentation.TweaksRoot import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.hosttokens.HostTokensRoot -import zed.rainxch.tweaks.presentation.mirror.AutoSuggestMirrorViewModel import zed.rainxch.tweaks.presentation.mirror.MirrorPickerRoot -import zed.rainxch.tweaks.presentation.mirror.components.AutoSuggestMirrorSheet import zed.rainxch.tweaks.presentation.skipped.SkippedUpdatesRoot -// Cross-screen "return result" key: set by the external-import wizard's -// "Add manually" path before navigateUp(), read once by the Apps screen. -private const val EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY = "external_import_open_link_sheet" - @Composable fun AppNavigation( navController: NavHostController, @@ -88,25 +83,39 @@ fun AppNavigation( ) { Row(modifier = Modifier.fillMaxSize()) { val desktopDrawerCurrent = - navController.currentBackStackEntryAsState().value.getCurrentScreen() + navController + .currentBackStackEntryAsState() + .value + .getCurrentScreen() if (isDesktop && desktopDrawerCurrent != null) { DesktopDrawer( currentScreen = desktopDrawerCurrent, onNavigate = { target -> navController.navigate(target) { - popUpTo(GithubStoreGraph.HomeScreen) { saveState = true } + popUpTo(GithubStoreGraph.HomeScreen) { + saveState = true + } launchSingleTop = true restoreState = true } }, isUpdateAvailable = - appsState.apps.any { it.installedApp.isUpdateAvailable } || - appsState.showImportProposalBanner, + appsState.apps.any { + it.installedApp.isUpdateAvailable + } || appsState.showImportProposalBanner, hasUnreadAnnouncements = announcementsUnreadCount > 0, ) } + Box( - modifier = if (isDesktop) Modifier.weight(1f).fillMaxHeight() else Modifier.fillMaxSize(), + modifier = + if (isDesktop) { + Modifier + .weight(1f) + .fillMaxHeight() + } else { + Modifier.fillMaxSize() + }, ) { NavHost( navController = navController, @@ -371,7 +380,11 @@ fun AppNavigation( navController.navigate(GithubStoreGraph.RecentlyViewedScreen) }, onNavigateToDevProfile = { username -> - navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username)) + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username, + ), + ) }, onNavigateToWhatsNew = { navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) @@ -492,12 +505,10 @@ fun AppNavigation( } composable { backStackEntry -> - // Pick up the "open link sheet" flag set by ExternalImportRoot's - // "Add manually" path. We consume the flag once on entry so a - // later config change or back-stack rewind doesn't reopen the sheet. LaunchedEffect(backStackEntry) { val handle = backStackEntry.savedStateHandle - val openLinkSheet = handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) + val openLinkSheet = + handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) if (openLinkSheet == true) { handle.remove(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) @@ -568,10 +579,6 @@ fun AppNavigation( restoreState = true } }, - // Badge fires when either an update is waiting OR pending - // import candidates need review. The badge is a single dot - // — a union of the two conditions is honest "you have - // something to look at on this tab". isUpdateAvailable = appsState.apps.any { it.installedApp.isUpdateAvailable } || appsState.showImportProposalBanner, @@ -587,22 +594,6 @@ fun AppNavigation( }, ) } - - val autoSuggestVm: AutoSuggestMirrorViewModel = koinViewModel() - val isAutoSuggestVisible by autoSuggestVm.isVisible.collectAsStateWithLifecycle() - if (isAutoSuggestVisible) { - AutoSuggestMirrorSheet( - onDismiss = autoSuggestVm::dismiss, - onPickOne = { - autoSuggestVm.onPickOneClicked() - navController.navigate(GithubStoreGraph.MirrorPickerScreen) { - launchSingleTop = true - } - }, - onMaybeLater = autoSuggestVm::onMaybeLater, - onDontAskAgain = autoSuggestVm::onDontAskAgain, - ) - } } } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index 9ce59dd6e..da7d7f914 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -9,11 +9,6 @@ sealed interface GithubStoreGraph { @Serializable data class SearchScreen( - // String over enum: Compose Navigation's Desktop (`nonAndroid.kt`) - // serializer needs an explicit NavType for non-primitive nav args, - // which enums don't have out of the box. Enum-as-name string keeps - // the contract type-safe at the caller / VM boundary while letting - // the route serialize on every target with no typeMap. val initialPlatform: String? = null, ) : GithubStoreGraph @@ -26,9 +21,6 @@ sealed interface GithubStoreGraph { val owner: String = "", val repo: String = "", val isComingFromUpdate: Boolean = false, - // Non-null when the repo lives on a non-GitHub forge (Codeberg / - // Forgejo / Gitea / custom). Drives the foreign-source branch in - // DetailsViewModel so we hit the Forgejo API instead of GitHub. val sourceHost: String? = null, ) : GithubStoreGraph @@ -82,12 +74,6 @@ sealed interface GithubStoreGraph { @Serializable data object HostTokensScreen : GithubStoreGraph - /** - * Category-specific list view powering Home's "See all" jumps. `category` is - * a free-form string mapped to a [zed.rainxch.home.domain.model.HomeCategory] - * at the destination (no enum nav arg — same Desktop nav serializer caveat - * as [SearchScreen.initialPlatform]). - */ @Serializable data class CategoryListScreen( val category: String, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 6e3c6e2cb..33c4d2efd 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -23,17 +23,10 @@ interface TweaksRepository { suspend fun setAmoledTheme(enabled: Boolean) - /** - * Unified two-axis theme mode (LIGHT / DARK / AMOLED / SYSTEM) derived from - * the underlying [getIsDarkTheme] (null = SYSTEM, false = LIGHT, true = DARK) - * and [getAmoledTheme] (true only when DARK, lifts to AMOLED). Setter splits - * the value back into the two boolean keys — no schema migration required. - */ fun getThemeMode(): Flow suspend fun setThemeMode(mode: ThemeMode) - /** One-shot first-launch onboarding completion flag (D17). */ fun getOnboardingComplete(): Flow suspend fun setOnboardingComplete(complete: Boolean) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt index 91b777c6a..fbf8165f1 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt @@ -16,10 +16,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.vocabulary.Squiggle -/** - * Section header with leading glyph + Fraunces italic title + optional sub-count + - * Squiggle underline + optional "See all ›" affordance (DESIGN.md §7.5). - */ @Composable fun SectionHeader( title: String, @@ -38,6 +34,7 @@ fun SectionHeader( verticalAlignment = Alignment.CenterVertically, ) { leading?.invoke() + Text( text = title, style = MaterialTheme.typography.titleLarge.copy( @@ -47,6 +44,7 @@ fun SectionHeader( ), color = MaterialTheme.colorScheme.onSurface, ) + if (subCount != null) { Text( text = "· $subCount", @@ -54,6 +52,7 @@ fun SectionHeader( style = MaterialTheme.typography.bodyMedium, ) } + if (onSeeAll != null) { Text( text = "See all ›", diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt index d55ed9272..0b82925c2 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt @@ -274,3 +274,5 @@ fun ExternalImportRoot( } } } + +const val EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY = "external_import_open_link_sheet" diff --git a/feature/home/presentation/build.gradle.kts b/feature/home/presentation/build.gradle.kts index 4b8703a77..43e0e0811 100644 --- a/feature/home/presentation/build.gradle.kts +++ b/feature/home/presentation/build.gradle.kts @@ -12,6 +12,7 @@ kotlin { implementation(projects.core.presentation) implementation(projects.feature.home.domain) implementation(projects.feature.profile.domain) + implementation(projects.feature.details.domain) implementation(libs.kotlinx.collections.immutable) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt index 974138d53..9937b1f4d 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt @@ -18,16 +18,6 @@ sealed interface HomeAction { data object OnPlatformPopupDismiss : HomeAction - data object OnSelectAllPlatforms : HomeAction - - data class OnPlatformToggle( - val platform: DiscoveryPlatform, - ) : HomeAction - - data class OnPlatformsSelected( - val platforms: Set, - ) : HomeAction - data class OnRepoClick( val repo: GithubRepoSummaryUi, ) : HomeAction diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index d83726485..121db5e48 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -55,6 +55,7 @@ import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.toIcons import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.home.domain.model.HomeCategory import zed.rainxch.home.presentation.components.HomeTopBar import zed.rainxch.home.presentation.components.HotCardItem import zed.rainxch.home.presentation.components.LeadCard @@ -71,7 +72,7 @@ fun HomeRoot( onNavigateToApps: () -> Unit, onNavigateToDetails: (repoId: Long) -> Unit, onNavigateToDeveloperProfile: (username: String) -> Unit, - onNavigateToCategoryList: (zed.rainxch.home.domain.model.HomeCategory) -> Unit, + onNavigateToCategoryList: (HomeCategory) -> Unit, onNavigateToStarredRepos: () -> Unit, viewModel: HomeViewModel = koinViewModel(), ) { @@ -101,13 +102,13 @@ fun HomeRoot( HomeAction.OnSettingsClick -> onNavigateToSettings() HomeAction.OnAppsClick -> onNavigateToApps() HomeAction.OnSeeAllHot -> onNavigateToCategoryList( - zed.rainxch.home.domain.model.HomeCategory.HOT_RELEASE, + HomeCategory.HOT_RELEASE, ) HomeAction.OnSeeAllTrending -> onNavigateToCategoryList( - zed.rainxch.home.domain.model.HomeCategory.TRENDING, + HomeCategory.TRENDING, ) HomeAction.OnSeeAllPopular -> onNavigateToCategoryList( - zed.rainxch.home.domain.model.HomeCategory.MOST_POPULAR, + HomeCategory.MOST_POPULAR, ) HomeAction.OnSeeAllStarred -> onNavigateToStarredRepos() is HomeAction.OnRepoClick -> onNavigateToDetails(action.repo.id) @@ -203,9 +204,6 @@ private fun FeedContent( val visibleStarred by visibleReposState(state, state.starred) val lead = visibleHot.firstOrNull() - // Limit sections on Home to keep the surface tight — full list lives in - // CategoryListScreen via "See all". Pick 6 as a balance between density - // and "skim-and-go" Home feel. val homeLimit = 6 val hotTail = visibleHot.drop(1).take(homeLimit) val trendingPreview = visibleTrending.take(homeLimit) @@ -230,10 +228,7 @@ private fun FeedContent( verticalArrangement = Arrangement.spacedBy(8.dp), ) { item(key = "top_bar") { - HomeTopBar( - onSearchClick = { onAction(HomeAction.OnSearchClick) }, - onSettingsClick = { onAction(HomeAction.OnSettingsClick) }, - ) + HomeTopBar() } if (lead != null) { @@ -333,53 +328,6 @@ private fun FeedContent( } } -@Composable -private fun PlatformFilterAction( - selectedPlatforms: Set, - isPlatformPopupVisible: Boolean, - onAction: (HomeAction) -> Unit, -) { - Box { - val icons = selectedPlatformsIcons(selectedPlatforms) - Row( - modifier = Modifier - .clip(RoundedCornerShape(14.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .clickable { onAction(HomeAction.OnPlatformPopupOpen) } - .padding(horizontal = 10.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - icons.forEach { icon -> - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurface, - ) - } - } - PlatformFilterMenu( - expanded = isPlatformPopupVisible, - selectedPlatforms = selectedPlatforms, - onDismiss = { onAction(HomeAction.OnPlatformPopupDismiss) }, - onSelectAll = { onAction(HomeAction.OnSelectAllPlatforms) }, - onToggle = { onAction(HomeAction.OnPlatformToggle(it)) }, - ) - } -} - -@Composable -private fun selectedPlatformsIcons( - selectedPlatforms: Set, -) = if (selectedPlatforms.isEmpty()) { - DiscoveryPlatform.All.toIcons() -} else { - DiscoveryPlatform.selectablePlatforms - .filter { it in selectedPlatforms } - .flatMap { it.toIcons() } -} - @Composable private fun visibleReposState( state: HomeState, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index b1aa3a04c..22ff33fed 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -2,10 +2,13 @@ package zed.rainxch.home.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -17,12 +20,12 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString +import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.GithubRepoSummary -import zed.rainxch.core.domain.model.GithubUser +import zed.rainxch.core.domain.model.InstalledApp import zed.rainxch.core.domain.model.Platform -import zed.rainxch.core.domain.model.StarredRepository as StarredRepositoryModel import zed.rainxch.core.domain.model.hasActualUpdate import zed.rainxch.core.domain.model.isReallyInstalled import zed.rainxch.core.domain.repository.FavouritesRepository @@ -35,6 +38,7 @@ import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ShareManager import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.utils.toUi +import zed.rainxch.details.domain.repository.DetailsRepository import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.home.domain.repository.HomeRepository import zed.rainxch.profile.domain.repository.ProfileRepository @@ -42,7 +46,6 @@ import zed.rainxch.profile.domain.repository.ProfileRepository class HomeViewModel( private val homeRepository: HomeRepository, private val installedAppsRepository: InstalledAppsRepository, - private val platform: Platform, private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, private val favouritesRepository: FavouritesRepository, private val starredRepository: StarredRepository, @@ -52,6 +55,7 @@ class HomeViewModel( private val seenReposRepository: SeenReposRepository, private val hiddenReposRepository: HiddenReposRepository, private val profileRepository: ProfileRepository, + private val detailsRepository: DetailsRepository, ) : ViewModel() { private var hasLoadedInitialData = false private var loadJob: Job? = null @@ -66,7 +70,6 @@ class HomeViewModel( observeCurrentUser() syncSystemState() - loadPlatform() refreshAllSections(isInitial = true) observeInstalledApps() observeFavourites() @@ -106,22 +109,6 @@ class HomeViewModel( _state.update { it.copy(isPlatformPopupVisible = false) } } - is HomeAction.OnPlatformToggle -> { - val target = _state.value.selectedPlatforms.toggle(action.platform) - applyPlatformSelection(target) - } - - is HomeAction.OnPlatformsSelected -> { - applyPlatformSelection(action.platforms) - } - - HomeAction.OnSelectAllPlatforms -> { - val current = _state.value.selectedPlatforms - val target = - if (current.isEmpty()) setOf(devicePlatformAsDiscovery()) else emptySet() - applyPlatformSelection(target) - } - is HomeAction.OnRepoLongClick -> { _state.update { it.copy(actionSheetRepoId = action.repoId) } } @@ -139,7 +126,7 @@ class HomeViewModel( _events.send(HomeEvent.OnMessage(getString(Res.string.failed_to_share_link))) return@launch } - if (platform != Platform.ANDROID) { + if (getPlatform() != Platform.ANDROID) { _events.send(HomeEvent.OnMessage(getString(Res.string.link_copied_to_clipboard))) } } @@ -220,16 +207,6 @@ class HomeViewModel( } } - private fun applyPlatformSelection(target: Set) { - if (target == _state.value.selectedPlatforms) return - viewModelScope.launch { - tweaksRepository.setDiscoveryPlatforms(target) - _state.update { it.copy(selectedPlatforms = target, isPlatformPopupVisible = false) } - refreshAllSections(isInitial = true) - _events.send(HomeEvent.OnScrollToListTop) - } - } - private fun refreshAllSections(isInitial: Boolean) { loadJob?.cancel() loadJob = @@ -326,13 +303,29 @@ class HomeViewModel( } try { runCatching { starredRepository.syncStarredRepos(forceRefresh = false) } - val top = starredRepository + val topIds = starredRepository .getAllStarred() .first() .sortedByDescending { it.stargazersCount } .take(5) - .map { it.toSummary() } - val mapped = mapReposToUi(top) + .map { it.repoId } + + // Fetch the full GithubRepoSummary per starred id from backend in parallel. + // The local StarredRepository row is a thin cache (no topics, updatedAt, + // availablePlatforms) so building UI from it would render half-empty cards + // and break TopicGlyph + FreshnessRing + PlatformGlyph. Failures per id are + // swallowed — the surface for that one repo just drops out. + val fetched = coroutineScope { + topIds + .map { id -> + async { + runCatching { detailsRepository.getRepositoryById(id) }.getOrNull() + } + } + .awaitAll() + .filterNotNull() + } + val mapped = mapReposToUi(fetched) _state.update { it.copy( starred = mapped.toImmutableList(), @@ -360,12 +353,6 @@ class HomeViewModel( } } - private fun loadPlatform() { - _state.update { - it.copy(isAppsSectionVisible = platform == Platform.ANDROID) - } - } - private fun observeInstalledApps() { viewModelScope.launch { installedAppsRepository.getAllInstalledApps().collect { installedApps -> @@ -509,33 +496,14 @@ class HomeViewModel( } } - private fun Set.toggle(platform: DiscoveryPlatform): Set { - if (platform == DiscoveryPlatform.All) return emptySet() - if (isEmpty()) return setOf(platform) - val mutated = if (platform in this) this - platform else this + platform - return when { - mutated.size == DiscoveryPlatform.selectablePlatforms.size -> emptySet() - mutated.isEmpty() -> setOf(devicePlatformAsDiscovery()) - else -> mutated - } - } - - private fun devicePlatformAsDiscovery(): DiscoveryPlatform = - when (platform) { - Platform.ANDROID -> DiscoveryPlatform.Android - Platform.WINDOWS -> DiscoveryPlatform.Windows - Platform.MACOS -> DiscoveryPlatform.Macos - Platform.LINUX -> DiscoveryPlatform.Linux - } - override fun onCleared() { super.onCleared() loadJob?.cancel() } } -private fun kotlinx.collections.immutable.ImmutableList.restamp( - installedMap: Map>, +private fun ImmutableList.restamp( + installedMap: Map>, ) = map { repo -> val apps = installedMap[repo.repository.id].orEmpty() repo.copy( @@ -544,19 +512,19 @@ private fun kotlinx.collections.immutable.ImmutableList.r ) }.toImmutableList() -private fun kotlinx.collections.immutable.ImmutableList.restampSeen( +private fun ImmutableList.restampSeen( ids: Set, ) = map { it.copy(isSeen = it.repository.id in ids) }.toImmutableList() -private fun kotlinx.collections.immutable.ImmutableList.restampFavourite( +private fun ImmutableList.restampFavourite( ids: Set, ) = map { it.copy(isFavourite = it.repository.id in ids) }.toImmutableList() -private fun kotlinx.collections.immutable.ImmutableList.restampStarred( +private fun ImmutableList.restampStarred( ids: Set, ) = map { it.copy(isStarred = it.repository.id in ids) }.toImmutableList() -private fun kotlinx.collections.immutable.ImmutableList.restampOwner( +private fun ImmutableList.restampOwner( login: String?, ) = map { repo -> repo.copy( @@ -565,28 +533,3 @@ private fun kotlinx.collections.immutable.ImmutableList.r ) }.toImmutableList() -private fun StarredRepositoryModel.toSummary(): GithubRepoSummary = - GithubRepoSummary( - id = repoId, - name = repoName, - fullName = "$repoOwner/$repoName", - owner = GithubUser( - id = 0L, - login = repoOwner, - avatarUrl = repoOwnerAvatarUrl, - htmlUrl = "https://github.com/$repoOwner", - ), - description = repoDescription, - defaultBranch = "main", - htmlUrl = repoUrl, - stargazersCount = stargazersCount, - forksCount = forksCount, - language = primaryLanguage, - topics = emptyList(), - releasesUrl = "$repoUrl/releases", - updatedAt = "", - isFork = false, - availablePlatforms = emptyList(), - downloadCount = 0, - sourceHost = null, - ) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt index f45134b80..f5ee0b325 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt @@ -4,10 +4,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,17 +13,9 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import zed.rainxch.core.presentation.components.buttons.IconButton -/** - * Home top bar — "Discover" title + Search + Settings actions. Platform filter - * lives in Tweaks → Discovery (per maintainer call, P12). Cookie brand mark moved - * to the Desktop drawer; the bottom nav carries the Cookie identity on Android. - */ @Composable fun HomeTopBar( - onSearchClick: () -> Unit, - onSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -47,19 +35,5 @@ fun HomeTopBar( color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), ) - IconButton(onClick = onSearchClick) { - Icon( - imageVector = Icons.Outlined.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurface, - ) - } - IconButton(onClick = onSettingsClick) { - Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onSurface, - ) - } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/AutoSuggestMirrorViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/AutoSuggestMirrorViewModel.kt deleted file mode 100644 index 304b32f64..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/AutoSuggestMirrorViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package zed.rainxch.tweaks.presentation.mirror - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import zed.rainxch.core.domain.network.SlowDownloadDetector -import zed.rainxch.core.domain.repository.MirrorRepository - -class AutoSuggestMirrorViewModel( - private val detector: SlowDownloadDetector, - private val mirrorRepository: MirrorRepository, -) : ViewModel() { - private val _isVisible = MutableStateFlow(false) - val isVisible = _isVisible.asStateFlow() - - init { - viewModelScope.launch { - detector.suggestMirror.collect { - _isVisible.value = true - } - } - } - - fun onMaybeLater() { - _isVisible.value = false - viewModelScope.launch { - mirrorRepository.snoozeAutoSuggest(24L * 60 * 60 * 1000) - } - } - - fun onDontAskAgain() { - _isVisible.value = false - viewModelScope.launch { - mirrorRepository.dismissAutoSuggestPermanently() - } - } - - fun onPickOneClicked() { - _isVisible.value = false - } - - fun dismiss() { - _isVisible.value = false - } -} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/AutoSuggestMirrorSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/AutoSuggestMirrorSheet.kt deleted file mode 100644 index ba5271e0f..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/AutoSuggestMirrorSheet.kt +++ /dev/null @@ -1,75 +0,0 @@ -package zed.rainxch.tweaks.presentation.mirror.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.mirror_auto_suggest_body -import zed.rainxch.githubstore.core.presentation.res.mirror_auto_suggest_dont_ask_again -import zed.rainxch.githubstore.core.presentation.res.mirror_auto_suggest_maybe_later -import zed.rainxch.githubstore.core.presentation.res.mirror_auto_suggest_pick_one -import zed.rainxch.githubstore.core.presentation.res.mirror_auto_suggest_title - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AutoSuggestMirrorSheet( - onDismiss: () -> Unit, - onPickOne: () -> Unit, - onMaybeLater: () -> Unit, - onDontAskAgain: () -> Unit, -) { - val sheetState = rememberModalBottomSheetState() - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(24.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Text( - text = stringResource(Res.string.mirror_auto_suggest_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.mirror_auto_suggest_body), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Button( - onClick = onPickOne, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(Res.string.mirror_auto_suggest_pick_one)) - } - OutlinedButton( - onClick = onMaybeLater, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(Res.string.mirror_auto_suggest_maybe_later)) - } - TextButton( - onClick = onDontAskAgain, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(Res.string.mirror_auto_suggest_dont_ask_again)) - } - } - } -} From 6e8d2f748b1483ec4aef103d5f7700bf0dd6ea27 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 15:52:10 +0500 Subject: [PATCH 026/172] fix(home): keep starred fetch in-module via HomeRepository.getRepositoryById --- feature/apps/data/build.gradle.kts | 10 ----- feature/apps/domain/build.gradle.kts | 10 ----- .../data/repository/HomeRepositoryImpl.kt | 45 +++++++++++++++++++ .../home/domain/repository/HomeRepository.kt | 8 ++++ feature/home/presentation/build.gradle.kts | 11 ----- .../home/presentation/HomeViewModel.kt | 9 +--- 6 files changed, 54 insertions(+), 39 deletions(-) diff --git a/feature/apps/data/build.gradle.kts b/feature/apps/data/build.gradle.kts index f452adf3a..7f4cb80cb 100644 --- a/feature/apps/data/build.gradle.kts +++ b/feature/apps/data/build.gradle.kts @@ -19,15 +19,5 @@ kotlin { implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/apps/domain/build.gradle.kts b/feature/apps/domain/build.gradle.kts index dee8adc57..6df97f433 100644 --- a/feature/apps/domain/build.gradle.kts +++ b/feature/apps/domain/build.gradle.kts @@ -12,15 +12,5 @@ kotlin { implementation(libs.kotlinx.coroutines.core) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt index fe55e0383..1040fb7ad 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt @@ -27,6 +27,8 @@ import zed.rainxch.core.data.cache.CacheManager import zed.rainxch.core.data.cache.CacheManager.CacheTtl.HOME_REPOS import zed.rainxch.core.data.dto.GithubRepoNetworkModel import zed.rainxch.core.data.dto.GithubRepoSearchResponse +import zed.rainxch.core.data.dto.RepoByIdNetwork +import zed.rainxch.core.domain.model.GithubUser import zed.rainxch.core.data.mappers.toSummary import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.executeRequest @@ -669,4 +671,47 @@ class HomeRepositoryImpl( private data class AssetNetworkModel( val name: String, ) + + override suspend fun getRepositoryById(id: Long): GithubRepoSummary? { + val cacheKey = "home:repo_id:$id" + cacheManager.get(cacheKey)?.let { return it } + return try { + val result = httpClient + .executeRequest { + get("/repositories/$id") { + header("Accept", "application/vnd.github+json") + } + }.getOrThrow() + .toSummary() + cacheManager.put(cacheKey, result, HOME_REPOS) + result + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + cacheManager.getStale(cacheKey)?.let { return it } + logger.warn("getRepositoryById($id) failed: ${e.message}") + null + } + } + + private fun RepoByIdNetwork.toSummary(): GithubRepoSummary = GithubRepoSummary( + id = id, + name = name, + fullName = fullName, + owner = GithubUser( + id = owner.id, + login = owner.login, + avatarUrl = owner.avatarUrl, + htmlUrl = owner.htmlUrl, + ), + description = description, + defaultBranch = defaultBranch, + htmlUrl = htmlUrl, + stargazersCount = stars, + forksCount = forks, + language = language, + topics = topics.orEmpty(), + releasesUrl = "https://api.github.com/repos/$fullName/releases{/id}", + updatedAt = updatedAt, + ) } diff --git a/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt b/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt index a9cd01f1b..199485dd7 100644 --- a/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt +++ b/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt @@ -31,4 +31,12 @@ interface HomeRepository { topic: TopicCategory, platforms: Set, ): Flow + + /** + * Fetch a single repo summary by id. Used by the Home starred section to + * hydrate stale local cache rows with fresh topics / updatedAt / platforms. + * Returns `null` on failure (network / 404) so callers can drop that one + * item without failing the whole section. + */ + suspend fun getRepositoryById(id: Long): zed.rainxch.core.domain.model.GithubRepoSummary? } diff --git a/feature/home/presentation/build.gradle.kts b/feature/home/presentation/build.gradle.kts index 43e0e0811..4fe62e016 100644 --- a/feature/home/presentation/build.gradle.kts +++ b/feature/home/presentation/build.gradle.kts @@ -12,7 +12,6 @@ kotlin { implementation(projects.core.presentation) implementation(projects.feature.home.domain) implementation(projects.feature.profile.domain) - implementation(projects.feature.details.domain) implementation(libs.kotlinx.collections.immutable) @@ -21,15 +20,5 @@ kotlin { implementation(libs.androidx.compose.ui.tooling.preview) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 22ff33fed..9e7705d78 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -38,7 +38,6 @@ import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ShareManager import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.utils.toUi -import zed.rainxch.details.domain.repository.DetailsRepository import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.home.domain.repository.HomeRepository import zed.rainxch.profile.domain.repository.ProfileRepository @@ -55,7 +54,6 @@ class HomeViewModel( private val seenReposRepository: SeenReposRepository, private val hiddenReposRepository: HiddenReposRepository, private val profileRepository: ProfileRepository, - private val detailsRepository: DetailsRepository, ) : ViewModel() { private var hasLoadedInitialData = false private var loadJob: Job? = null @@ -310,16 +308,11 @@ class HomeViewModel( .take(5) .map { it.repoId } - // Fetch the full GithubRepoSummary per starred id from backend in parallel. - // The local StarredRepository row is a thin cache (no topics, updatedAt, - // availablePlatforms) so building UI from it would render half-empty cards - // and break TopicGlyph + FreshnessRing + PlatformGlyph. Failures per id are - // swallowed — the surface for that one repo just drops out. val fetched = coroutineScope { topIds .map { id -> async { - runCatching { detailsRepository.getRepositoryById(id) }.getOrNull() + runCatching { homeRepository.getRepositoryById(id) }.getOrNull() } } .awaitAll() From d207b0567e0c833b83518d1b64de30918406187c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 16:01:20 +0500 Subject: [PATCH 027/172] =?UTF-8?q?build:=20cleanup=20gradle=20files=20?= =?UTF-8?q?=E2=80=94=20drop=20empty=20source=20sets=20+=20unused=20profile?= =?UTF-8?q?=20domain=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/data/build.gradle.kts | 15 +++++++++------ core/domain/build.gradle.kts | 10 ---------- core/presentation/build.gradle.kts | 5 ++--- feature/apps/data/build.gradle.kts | 4 ++-- feature/apps/domain/build.gradle.kts | 2 +- feature/apps/presentation/build.gradle.kts | 19 ++++--------------- feature/auth/data/build.gradle.kts | 10 ---------- feature/auth/domain/build.gradle.kts | 13 +------------ feature/auth/presentation/build.gradle.kts | 10 ---------- feature/details/data/build.gradle.kts | 12 +----------- feature/details/domain/build.gradle.kts | 10 ---------- feature/details/presentation/build.gradle.kts | 18 ++++-------------- feature/dev-profile/data/build.gradle.kts | 12 +----------- feature/dev-profile/domain/build.gradle.kts | 10 ---------- .../dev-profile/presentation/build.gradle.kts | 16 +++------------- feature/favourites/data/build.gradle.kts | 10 ---------- feature/favourites/domain/build.gradle.kts | 10 ---------- .../favourites/presentation/build.gradle.kts | 15 ++------------- feature/home/data/build.gradle.kts | 15 ++------------- feature/home/domain/build.gradle.kts | 3 +-- feature/home/presentation/build.gradle.kts | 5 +---- feature/profile/data/build.gradle.kts | 12 +----------- feature/profile/domain/build.gradle.kts | 13 +------------ feature/profile/presentation/build.gradle.kts | 11 ----------- .../presentation/build.gradle.kts | 10 ---------- feature/search/data/build.gradle.kts | 12 +----------- feature/search/domain/build.gradle.kts | 13 +------------ feature/search/presentation/build.gradle.kts | 5 +---- feature/starred/data/build.gradle.kts | 10 ---------- feature/starred/domain/build.gradle.kts | 10 ---------- feature/starred/presentation/build.gradle.kts | 3 +-- feature/tweaks/presentation/build.gradle.kts | 19 ++++--------------- 32 files changed, 44 insertions(+), 298 deletions(-) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 1b1cc6a0c..7d491d791 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.datetime) implementation(projects.core.domain) @@ -23,22 +24,24 @@ kotlin { implementation(libs.touchlab.kermit) - implementation(libs.datastore) - implementation(libs.datastore.preferences) implementation(libs.ksafe) - implementation(libs.kotlinx.datetime) + implementation(libs.datastore) + implementation(libs.datastore.preferences) } } androidMain { dependencies { - implementation(libs.ktor.client.okhttp) - implementation(libs.androidx.work.runtime) implementation(libs.shizuku.api) implementation(libs.shizuku.provider) - implementation(libs.dhizuku.api) compileOnly(libs.hidden.api.stub) + + implementation(libs.dhizuku.api) + + implementation(libs.ktor.client.okhttp) + + implementation(libs.androidx.work.runtime) } } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ae2bc844e..fa88f6f78 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -10,15 +10,5 @@ kotlin { implementation(libs.kotlinx.coroutines.core) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/core/presentation/build.gradle.kts b/core/presentation/build.gradle.kts index 1b3803c25..e54b45038 100644 --- a/core/presentation/build.gradle.kts +++ b/core/presentation/build.gradle.kts @@ -7,17 +7,16 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(libs.bundles.landscapist) implementation(libs.jetbrains.lifecycle.compose) - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.collections.immutable) implementation(libs.jetbrains.compose.components.resources) - implementation(libs.androidx.compose.ui.tooling.preview) } } diff --git a/feature/apps/data/build.gradle.kts b/feature/apps/data/build.gradle.kts index 7f4cb80cb..6a480bc1e 100644 --- a/feature/apps/data/build.gradle.kts +++ b/feature/apps/data/build.gradle.kts @@ -8,13 +8,13 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.feature.apps.domain) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.datetime) implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) } diff --git a/feature/apps/domain/build.gradle.kts b/feature/apps/domain/build.gradle.kts index 6df97f433..4a38ddd7f 100644 --- a/feature/apps/domain/build.gradle.kts +++ b/feature/apps/domain/build.gradle.kts @@ -7,9 +7,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) - implementation(libs.kotlinx.coroutines.core) } } } diff --git a/feature/apps/presentation/build.gradle.kts b/feature/apps/presentation/build.gradle.kts index 547bb0666..25a387cd9 100644 --- a/feature/apps/presentation/build.gradle.kts +++ b/feature/apps/presentation/build.gradle.kts @@ -7,28 +7,17 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.datetime) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.apps.domain) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.compose.components.resources) - implementation(libs.bundles.landscapist) - implementation(libs.kotlinx.collections.immutable) - implementation(libs.kotlinx.datetime) - } - } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.jetbrains.compose.components.resources) } } } diff --git a/feature/auth/data/build.gradle.kts b/feature/auth/data/build.gradle.kts index a65fc7038..c48ad0870 100644 --- a/feature/auth/data/build.gradle.kts +++ b/feature/auth/data/build.gradle.kts @@ -17,15 +17,5 @@ kotlin { implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/auth/domain/build.gradle.kts b/feature/auth/domain/build.gradle.kts index b5a257f7a..4a38ddd7f 100644 --- a/feature/auth/domain/build.gradle.kts +++ b/feature/auth/domain/build.gradle.kts @@ -7,20 +7,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) - - implementation(projects.core.domain) - implementation(libs.kotlinx.coroutines.core) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(projects.core.domain) } } } diff --git a/feature/auth/presentation/build.gradle.kts b/feature/auth/presentation/build.gradle.kts index 5db6ce74b..12d920187 100644 --- a/feature/auth/presentation/build.gradle.kts +++ b/feature/auth/presentation/build.gradle.kts @@ -16,15 +16,5 @@ kotlin { implementation(libs.jetbrains.compose.components.resources) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/details/data/build.gradle.kts b/feature/details/data/build.gradle.kts index cb6ef330e..ea873112a 100644 --- a/feature/details/data/build.gradle.kts +++ b/feature/details/data/build.gradle.kts @@ -8,25 +8,15 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.feature.details.domain) - implementation(libs.kotlinx.coroutines.core) implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/details/domain/build.gradle.kts b/feature/details/domain/build.gradle.kts index 7ab581f42..0bac5a50c 100644 --- a/feature/details/domain/build.gradle.kts +++ b/feature/details/domain/build.gradle.kts @@ -11,15 +11,5 @@ kotlin { implementation(projects.core.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/details/presentation/build.gradle.kts b/feature/details/presentation/build.gradle.kts index 193f55d39..6ea23707f 100644 --- a/feature/details/presentation/build.gradle.kts +++ b/feature/details/presentation/build.gradle.kts @@ -7,32 +7,22 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.datetime) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.details.domain) - implementation(projects.feature.profile.domain) implementation(libs.markdown.renderer) implementation(libs.markdown.renderer.coil3) implementation(libs.highlights) - implementation(libs.ktor.client.core) - implementation(libs.jetbrains.compose.components.resources) - implementation(libs.kotlinx.datetime) + implementation(libs.ktor.client.core) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.bundles.landscapist) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(libs.jetbrains.compose.components.resources) + implementation(libs.androidx.compose.ui.tooling.preview) } } } diff --git a/feature/dev-profile/data/build.gradle.kts b/feature/dev-profile/data/build.gradle.kts index c12ec34eb..2daab4121 100644 --- a/feature/dev-profile/data/build.gradle.kts +++ b/feature/dev-profile/data/build.gradle.kts @@ -8,25 +8,15 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.feature.devProfile.domain) - implementation(libs.kotlinx.coroutines.core) implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/dev-profile/domain/build.gradle.kts b/feature/dev-profile/domain/build.gradle.kts index 7ab581f42..0bac5a50c 100644 --- a/feature/dev-profile/domain/build.gradle.kts +++ b/feature/dev-profile/domain/build.gradle.kts @@ -11,15 +11,5 @@ kotlin { implementation(projects.core.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/dev-profile/presentation/build.gradle.kts b/feature/dev-profile/presentation/build.gradle.kts index 8eb6e5587..d93d82d04 100644 --- a/feature/dev-profile/presentation/build.gradle.kts +++ b/feature/dev-profile/presentation/build.gradle.kts @@ -7,26 +7,16 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.devProfile.domain) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.compose.components.resources) - implementation(libs.bundles.landscapist) - implementation(libs.kotlinx.collections.immutable) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.jetbrains.compose.components.resources) } } } diff --git a/feature/favourites/data/build.gradle.kts b/feature/favourites/data/build.gradle.kts index 4cab43faa..6ac324cf6 100644 --- a/feature/favourites/data/build.gradle.kts +++ b/feature/favourites/data/build.gradle.kts @@ -13,15 +13,5 @@ kotlin { implementation(projects.feature.favourites.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/favourites/domain/build.gradle.kts b/feature/favourites/domain/build.gradle.kts index 7ab581f42..0bac5a50c 100644 --- a/feature/favourites/domain/build.gradle.kts +++ b/feature/favourites/domain/build.gradle.kts @@ -11,15 +11,5 @@ kotlin { implementation(projects.core.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/favourites/presentation/build.gradle.kts b/feature/favourites/presentation/build.gradle.kts index e66031a15..dc41938f1 100644 --- a/feature/favourites/presentation/build.gradle.kts +++ b/feature/favourites/presentation/build.gradle.kts @@ -7,27 +7,16 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.favourites.domain) - implementation(projects.feature.profile.domain) - - implementation(libs.bundles.landscapist) - implementation(libs.kotlinx.collections.immutable) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(libs.bundles.landscapist) } } } diff --git a/feature/home/data/build.gradle.kts b/feature/home/data/build.gradle.kts index fd7b5970e..dc4a98681 100644 --- a/feature/home/data/build.gradle.kts +++ b/feature/home/data/build.gradle.kts @@ -8,27 +8,16 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.feature.home.domain) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.datetime) - implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/home/domain/build.gradle.kts b/feature/home/domain/build.gradle.kts index b5a257f7a..d400db432 100644 --- a/feature/home/domain/build.gradle.kts +++ b/feature/home/domain/build.gradle.kts @@ -7,10 +7,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) - - implementation(libs.kotlinx.coroutines.core) } } diff --git a/feature/home/presentation/build.gradle.kts b/feature/home/presentation/build.gradle.kts index 4fe62e016..3a5fcb2df 100644 --- a/feature/home/presentation/build.gradle.kts +++ b/feature/home/presentation/build.gradle.kts @@ -7,16 +7,13 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.home.domain) - implementation(projects.feature.profile.domain) - - implementation(libs.kotlinx.collections.immutable) implementation(libs.jetbrains.compose.components.resources) - implementation(libs.androidx.compose.ui.tooling.preview) } } diff --git a/feature/profile/data/build.gradle.kts b/feature/profile/data/build.gradle.kts index 5c6431d62..4040b24de 100644 --- a/feature/profile/data/build.gradle.kts +++ b/feature/profile/data/build.gradle.kts @@ -8,6 +8,7 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) implementation(projects.core.data) @@ -15,17 +16,6 @@ kotlin { implementation(libs.bundles.koin.common) implementation(libs.bundles.ktor.common) - implementation(libs.kotlinx.coroutines.core) - } - } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { } } } diff --git a/feature/profile/domain/build.gradle.kts b/feature/profile/domain/build.gradle.kts index b5a257f7a..4a38ddd7f 100644 --- a/feature/profile/domain/build.gradle.kts +++ b/feature/profile/domain/build.gradle.kts @@ -7,20 +7,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) - - implementation(projects.core.domain) - implementation(libs.kotlinx.coroutines.core) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(projects.core.domain) } } } diff --git a/feature/profile/presentation/build.gradle.kts b/feature/profile/presentation/build.gradle.kts index 655f418f9..add8825be 100644 --- a/feature/profile/presentation/build.gradle.kts +++ b/feature/profile/presentation/build.gradle.kts @@ -14,17 +14,6 @@ kotlin { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) - - } - } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { } } } diff --git a/feature/recently-viewed/presentation/build.gradle.kts b/feature/recently-viewed/presentation/build.gradle.kts index 614614048..d6cb115a8 100644 --- a/feature/recently-viewed/presentation/build.gradle.kts +++ b/feature/recently-viewed/presentation/build.gradle.kts @@ -18,15 +18,5 @@ kotlin { implementation(libs.jetbrains.compose.components.resources) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/search/data/build.gradle.kts b/feature/search/data/build.gradle.kts index 34e191b23..c166e95e1 100644 --- a/feature/search/data/build.gradle.kts +++ b/feature/search/data/build.gradle.kts @@ -8,25 +8,15 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.feature.search.domain) - implementation(libs.kotlinx.coroutines.core) implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/search/domain/build.gradle.kts b/feature/search/domain/build.gradle.kts index b5a257f7a..4a38ddd7f 100644 --- a/feature/search/domain/build.gradle.kts +++ b/feature/search/domain/build.gradle.kts @@ -7,20 +7,9 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) - - implementation(projects.core.domain) - implementation(libs.kotlinx.coroutines.core) - } - } - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { + implementation(projects.core.domain) } } } diff --git a/feature/search/presentation/build.gradle.kts b/feature/search/presentation/build.gradle.kts index 669d5ab03..ed014bd95 100644 --- a/feature/search/presentation/build.gradle.kts +++ b/feature/search/presentation/build.gradle.kts @@ -7,17 +7,14 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.search.domain) - implementation(projects.feature.profile.domain) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) - - implementation(libs.kotlinx.collections.immutable) } } } diff --git a/feature/starred/data/build.gradle.kts b/feature/starred/data/build.gradle.kts index dc5ff3ffb..73578f11d 100644 --- a/feature/starred/data/build.gradle.kts +++ b/feature/starred/data/build.gradle.kts @@ -13,15 +13,5 @@ kotlin { implementation(projects.feature.starred.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/starred/domain/build.gradle.kts b/feature/starred/domain/build.gradle.kts index 7ab581f42..0bac5a50c 100644 --- a/feature/starred/domain/build.gradle.kts +++ b/feature/starred/domain/build.gradle.kts @@ -11,15 +11,5 @@ kotlin { implementation(projects.core.domain) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/starred/presentation/build.gradle.kts b/feature/starred/presentation/build.gradle.kts index 1b65fc473..66efd49b3 100644 --- a/feature/starred/presentation/build.gradle.kts +++ b/feature/starred/presentation/build.gradle.kts @@ -7,6 +7,7 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.presentation) @@ -15,8 +16,6 @@ kotlin { implementation(libs.bundles.landscapist) - implementation(libs.kotlinx.collections.immutable) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) } diff --git a/feature/tweaks/presentation/build.gradle.kts b/feature/tweaks/presentation/build.gradle.kts index 882f70e35..67796677e 100644 --- a/feature/tweaks/presentation/build.gradle.kts +++ b/feature/tweaks/presentation/build.gradle.kts @@ -7,29 +7,18 @@ kotlin { commonMain { dependencies { implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.collections.immutable) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.core.presentation) - implementation(projects.feature.profile.domain) - - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.compose.components.resources) - implementation(libs.touchlab.kermit) - implementation(libs.kotlinx.collections.immutable) api(libs.ktor.client.core) - } - } - - androidMain { - dependencies { - } - } + implementation(libs.touchlab.kermit) - jvmMain { - dependencies { + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.jetbrains.compose.components.resources) } } } From 9c5972136f4f523f682741ba0e4332ab43b69875 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 19:34:39 +0500 Subject: [PATCH 028/172] feat: rip telemetry, device-identity, and analytics opt-in entirely --- .../zed/rainxch/githubstore/MainViewModel.kt | 8 +- .../githubstore/app/di/SharedModules.kt | 2 +- .../githubstore/app/di/ViewModelsModule.kt | 4 +- .../app/whatsnew/WhatsNewLoaderImpl.kt | 4 - .../rainxch/core/data/cache/CacheManager.kt | 26 -- .../zed/rainxch/core/data/di/SharedModule.kt | 99 ++---- .../zed/rainxch/core/data/dto/EventRequest.kt | 29 -- .../core/data/network/BackendApiClient.kt | 17 - .../core/data/network/GitHubClientProvider.kt | 8 +- .../core/data/network/HttpClientFactory.kt | 9 +- .../interceptor/UnauthorizedInterceptor.kt | 14 +- .../DeviceIdentityRepositoryImpl.kt | 87 ----- .../ExternalImportRepositoryImpl.kt | 54 --- .../repository/TelemetryRepositoryImpl.kt | 335 ------------------ .../data/repository/TweaksRepositoryImpl.kt | 5 - ...teImpl.kt => UserSessionRepositoryImpl.kt} | 6 +- .../repository/DeviceIdentityRepository.kt | 7 - .../domain/repository/SeenReposRepository.kt | 5 - .../domain/repository/TelemetryRepository.kt | 64 ---- .../domain/repository/TweaksRepository.kt | 4 - ...ationState.kt => UserSessionRepository.kt} | 2 +- .../core/domain/repository/WhatsNewLoader.kt | 11 - .../composeResources/values-ar/strings-ar.xml | 6 - .../composeResources/values-bn/strings-bn.xml | 6 - .../composeResources/values-es/strings-es.xml | 6 - .../composeResources/values-fr/strings-fr.xml | 6 - .../composeResources/values-hi/strings-hi.xml | 6 - .../composeResources/values-it/strings-it.xml | 6 - .../composeResources/values-ja/strings-ja.xml | 6 - .../composeResources/values-ko/strings-ko.xml | 6 - .../composeResources/values-pl/strings-pl.xml | 6 - .../composeResources/values-ru/strings-ru.xml | 6 - .../composeResources/values-tr/strings-tr.xml | 6 - .../values-zh-rCN/strings-zh-rCN.xml | 6 - .../composeResources/values/strings.xml | 6 - .../import/ExternalImportViewModel.kt | 34 +- .../starred/StarredPickerViewModel.kt | 6 +- .../details/presentation/DetailsViewModel.kt | 21 +- .../presentation/FavouritesViewModel.kt | 2 - .../rainxch/profile/data/di/SharedModule.kt | 2 +- .../data/repository/ProfileRepositoryImpl.kt | 7 +- .../rainxch/search/data/di/SharedModule.kt | 8 + .../repository/SearchHistoryRepositoryImpl.kt | 8 +- .../repository/SearchHistoryRepository.kt | 4 +- .../search/presentation/SearchViewModel.kt | 25 +- .../presentation/StarredReposViewModel.kt | 6 +- .../tweaks/presentation/TweaksAction.kt | 6 - .../tweaks/presentation/TweaksEvent.kt | 2 - .../rainxch/tweaks/presentation/TweaksRoot.kt | 6 - .../tweaks/presentation/TweaksState.kt | 1 - .../tweaks/presentation/TweaksViewModel.kt | 34 -- .../components/sections/Others.kt | 58 --- .../hosttokens/HostTokensViewModel.kt | 6 +- 53 files changed, 77 insertions(+), 1037 deletions(-) delete mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt delete mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt delete mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt rename core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/{AuthenticationStateImpl.kt => UserSessionRepositoryImpl.kt} (96%) delete mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt delete mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt rename core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/{AuthenticationState.kt => UserSessionRepository.kt} (91%) rename {core/data/src/commonMain/kotlin/zed/rainxch/core => feature/search/data/src/commonMain/kotlin/zed/rainxch/search}/data/repository/SearchHistoryRepositoryImpl.kt (90%) rename {core/domain/src/commonMain/kotlin/zed/rainxch/core => feature/search/domain/src/commonMain/kotlin/zed/rainxch}/domain/repository/SearchHistoryRepository.kt (84%) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt index 6ba54c78c..1d7c81b52 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt @@ -8,16 +8,16 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import zed.rainxch.core.domain.repository.AuthenticationState import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.RateLimitRepository import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase class MainViewModel( private val tweaksRepository: TweaksRepository, private val installedAppsRepository: InstalledAppsRepository, - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, private val rateLimitRepository: RateLimitRepository, private val syncUseCase: SyncInstalledAppsUseCase, ) : ViewModel() { @@ -26,7 +26,7 @@ class MainViewModel( init { viewModelScope.launch(Dispatchers.IO) { - authenticationState + userSessionRepository .isUserLoggedIn() .collect { isLoggedIn -> _state.update { it.copy(isLoggedIn = isLoggedIn) } @@ -107,7 +107,7 @@ class MainViewModel( } viewModelScope.launch { - authenticationState.sessionExpiredEvent.collect { + userSessionRepository.sessionExpiredEvent.collect { _state.update { it.copy(showSessionExpiredDialog = true) } } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt index 8cdcb9c74..401c7b5df 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt @@ -13,7 +13,7 @@ val mainModule: Module = installedAppsRepository = get(), rateLimitRepository = get(), syncUseCase = get(), - authenticationState = get(), + userSessionRepository = get(), ) } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index c62ac7570..49440b803 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -64,10 +64,9 @@ val viewModelsModule = installationManager = get(), attestationVerifier = get(), downloadOrchestrator = get(), - telemetryRepository = get(), externalImportRepository = get(), apkInspector = get(), - authenticationState = get(), + userSessionRepository = get(), systemInstallSerializer = get(), profileRepository = get(), ) @@ -90,7 +89,6 @@ val viewModelsModule = tweaksRepository = get(), seenReposRepository = get(), searchHistoryRepository = get(), - telemetryRepository = get(), profileRepository = get(), hiddenReposRepository = get(), initialPlatform = params.getOrNull(), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.kt index fd6d40a91..bb380c281 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.kt @@ -44,10 +44,6 @@ class WhatsNewLoaderImpl( versionCode: Int, languageTag: String?, ): List { - // Explicit tag passed in wins over global Locale lookup — - // prevents the race with MainActivity's `setActiveLanguageTag` - // when both this VM and MainActivity subscribe to the same - // `getAppLanguage()` flow (#526 follow-up). val (full, primary) = if (!languageTag.isNullOrBlank()) { languageTag to languageTag.substringBefore('-') diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt index 44336a0f6..3ed3f3674 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/cache/CacheManager.kt @@ -24,11 +24,6 @@ class CacheManager( encodeDefaults = true } - // Guarded by [memoryCacheMutex]. Multiple repositories read/write - // this map from concurrent ViewModel scopes — without the mutex a - // structural HashMap mutation can corrupt internal state on rehash. - // `@PublishedApi internal` so the inline reified `get` / `put` can - // touch them without leaking the map to outside callers. @PublishedApi internal val memoryCache = HashMap>() @@ -37,9 +32,6 @@ class CacheManager( init { appScope?.launch { - // Periodic janitor: in-memory cache grows otherwise unbounded - // over a long session. Run once an hour off the critical path; - // mutex serialises with foreground reads/writes. while (true) { delay(CLEANUP_INTERVAL_MS) runCatching { cleanupExpired() } @@ -59,11 +51,6 @@ class CacheManager( return try { json.decodeFromString(serializer(), jsonData) } catch (_: Exception) { - // Guarded eviction: only remove if no concurrent - // `put` has replaced our snapshot. Without the - // equality check, a fresh value written between - // the snapshot read and this decode failure would - // be wrongly evicted. memoryCacheMutex.withLock { if (memoryCache[key] == cached) memoryCache.remove(key) } @@ -85,9 +72,6 @@ class CacheManager( return try { json.decodeFromString(serializer(), entry.jsonData) } catch (_: Exception) { - // Same race rationale as the in-memory branch — use the - // row's cachedAt as a version stamp so a fresh `put` that - // raced in between getValid and now is preserved. cacheDao.deleteIfMatches(key, entry.cachedAt) memoryCacheMutex.withLock { if (memoryCache[key] == snapshot) memoryCache.remove(key) @@ -133,14 +117,6 @@ class CacheManager( cacheDao.delete(key) } - suspend fun invalidateByPrefix(prefix: String) { - memoryCacheMutex.withLock { - val keysToRemove = memoryCache.keys.filter { it.startsWith(prefix) } - keysToRemove.forEach { memoryCache.remove(it) } - } - cacheDao.deleteByPrefix(prefix) - } - suspend fun clearAll() { memoryCacheMutex.withLock { memoryCache.clear() } cacheDao.deleteAll() @@ -159,8 +135,6 @@ class CacheManager( } companion object CacheTtl { - // Tuned per E4 perf-pass spec: home grid is browsed often + tolerates - // stale data well; longer TTL cuts cold-start network round-trips. val HOME_REPOS = 24.hours.inWholeMilliseconds val REPO_DETAILS = 6.hours.inWholeMilliseconds val RELEASES = 6.hours.inWholeMilliseconds diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index eadd97408..83e433285 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import named import org.koin.core.qualifier.named import org.koin.dsl.module import zed.rainxch.core.data.network.createPlatformHttpClient @@ -50,15 +51,13 @@ import zed.rainxch.core.data.network.ProxyManager import zed.rainxch.core.data.network.ProxyManagerSeeding import zed.rainxch.core.data.network.ProxyTesterImpl import zed.rainxch.core.data.network.TranslationClientProvider -import zed.rainxch.core.data.repository.AuthenticationStateImpl +import zed.rainxch.core.data.repository.UserSessionRepositoryImpl import zed.rainxch.core.data.repository.ExternalImportRepositoryImpl import zed.rainxch.core.data.repository.FavouritesRepositoryImpl import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl import zed.rainxch.core.data.repository.ProxyRepositoryImpl -import zed.rainxch.core.data.repository.DeviceIdentityRepositoryImpl import zed.rainxch.core.data.repository.RateLimitRepositoryImpl import zed.rainxch.core.data.repository.SearchHistoryRepositoryImpl -import zed.rainxch.core.data.repository.TelemetryRepositoryImpl import zed.rainxch.core.data.repository.HiddenReposRepositoryImpl import zed.rainxch.core.data.repository.SeenReposRepositoryImpl import zed.rainxch.core.data.repository.StarredRepositoryImpl @@ -79,8 +78,7 @@ import zed.rainxch.core.domain.system.ExternalAppScanner import zed.rainxch.core.domain.system.MultiSourceDownloader import zed.rainxch.core.domain.repository.AnnouncementsCacheStore import zed.rainxch.core.domain.repository.AnnouncementsRepository -import zed.rainxch.core.domain.repository.AuthenticationState -import zed.rainxch.core.domain.repository.DeviceIdentityRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.ExternalImportRepository import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository @@ -91,7 +89,6 @@ import zed.rainxch.core.domain.repository.SearchHistoryRepository import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase @@ -109,8 +106,8 @@ val coreModule = getPlatform() } - single { - AuthenticationStateImpl( + single { + UserSessionRepositoryImpl( tokenStore = get(), ) } @@ -152,7 +149,7 @@ val coreModule = single { TweaksRepositoryImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), + ksafe = get(qualifier = named("prefs")), legacyDataStore = get(), ) } @@ -163,8 +160,8 @@ val coreModule = single { AnnouncementsCacheStoreImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("announcements_cache")), - legacyDataStore = get(qualifier = org.koin.core.qualifier.named("announcements")), + ksafe = get(qualifier = named("announcements_cache")), + legacyDataStore = get(qualifier = named("announcements")), ) } @@ -187,13 +184,11 @@ val coreModule = single { val repo = MirrorRepositoryImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), + ksafe = get(qualifier = named("prefs")), legacyDataStore = get(), apiClient = get(), appScope = get(), ) - // Kick off the ProxyManager mirror-template snapshot collector - // so the Ktor interceptor can resolve the template synchronously. ProxyManager.startMirrorCollector(repo, get()) repo } @@ -210,15 +205,9 @@ val coreModule = ) } - single { - SearchHistoryRepositoryImpl( - searchHistoryDao = get(), - ) - } - single { ProxyRepositoryImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), + ksafe = get(qualifier = named("prefs")), legacyDataStore = get(), logger = get(), ) @@ -238,47 +227,19 @@ val coreModule = } single { - CacheManager(cacheDao = get(), appScope = get()) + CacheManager( + cacheDao = get(), + appScope = get() + ) } single { - // Request the seeding sentinel so Koin guarantees ProxyManager - // has the user's persisted config loaded before we snapshot - // the discovery flow for the initial client build. get() BackendApiClient( proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY), tokenStore = get(), ) } - // NOTE: the reviewer asked for a Koin onClose hook to call - // BackendApiClient.close()/GitHubClientProvider.close()/ - // TranslationClientProvider.close() at Koin shutdown. Koin 4.x - // (4.1.1 on this project) doesn't expose that hook at the - // module DSL level — it existed in 3.x and was removed — and - // there's no clean replacement short of wrapping each provider - // in a Koin scope. On Android/Desktop the process exit - // releases these resources anyway, so we intentionally leave - // the hooks off rather than fake them with an API that doesn't - // fit. Revisit if we upgrade Koin or adopt scope-based DI. - - single { - DeviceIdentityRepositoryImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), - legacyDataStore = get(), - ) - } - - single { - TelemetryRepositoryImpl( - backendApiClient = get(), - deviceIdentity = get(), - tweaksRepository = get(), - platform = get(), - appScope = get(), - logger = get(), - ) - } single { BackendExternalMatchApi(get()) } @@ -298,11 +259,10 @@ val coreModule = scanner = get(), externalLinkDao = get(), signingFingerprintDao = get(), - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), + ksafe = get(qualifier = named("prefs")), legacyDataStore = get(), externalMatchApi = get(), backendClient = get(), - telemetry = get(), forgejoClientRegistry = get(), tweaksRepository = get(), ) @@ -316,14 +276,11 @@ val coreModule = single { SlowDownloadDetectorImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("prefs")), + ksafe = get(qualifier = named("prefs")), appScope = get(), ) } - // Application-scoped download / install orchestrator. Lives - // for the process lifetime so downloads survive screen - // navigation. ViewModels are observers, never owners. single { DefaultDownloadOrchestrator( downloader = get(), @@ -338,12 +295,6 @@ val coreModule = ) } - // Single-flight gate over `installer.install` calls that - // delegate to the system installer dialog. Without this, - // sequential update flows fire stacked ACTION_VIEW intents - // and OEM PackageInstaller activities (Xiaomi, OPPO, vivo, - // Honor, Samsung variants) routinely fail to refresh — the - // second dialog keeps showing the first APK. single { DefaultSystemInstallSerializer() } @@ -351,16 +302,6 @@ val coreModule = val networkModule = module { - // Seed [ProxyManager] from persisted per-scope configs *before* - // any HTTP client is constructed. Registered as its own - // [ProxyManagerSeeding] sentinel so client providers can depend - // on it explicitly — without this the seeding would live inside - // one provider's factory and silently race with others. - // - // Reads run in parallel under a single 1.5s budget (was 1.5s × 3 - // sequential). On timeout / DataStore failure we fall back to the - // in-memory defaults — we'd rather the app network with the - // System proxy than stall at launch on a slow disk. single(createdAtStart = true) { val repository = get() runBlocking { @@ -389,7 +330,7 @@ val networkModule = GitHubClientProvider( tokenStore = get(), rateLimitRepository = get(), - authenticationState = get(), + userSessionRepository = get(), proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY), ) } @@ -403,7 +344,7 @@ val networkModule = single { DefaultTokenStore( - ksafe = get(qualifier = org.koin.core.qualifier.named("tokens")), + ksafe = get(qualifier = named("tokens")), legacyDataStore = get(), ) } @@ -414,8 +355,8 @@ val networkModule = single { zed.rainxch.core.data.repository.HostTokenRepositoryImpl( - ksafe = get(qualifier = org.koin.core.qualifier.named("tokens")), - httpClient = get(qualifier = org.koin.core.qualifier.named("test")), + ksafe = get(qualifier = named("tokens")), + httpClient = get(qualifier = named("test")), ) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt deleted file mode 100644 index 3ff0b1c16..000000000 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/EventRequest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package zed.rainxch.core.data.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class EventRequest( - val deviceId: String, - val platform: String, - val appVersion: String? = null, - val eventType: String, - val repoId: Long? = null, - val resultCount: Int? = null, - val success: Boolean? = null, - val errorCode: String? = null, - // ── E1 external-import props (all bucketed enums or counts) ── - val trigger: String? = null, - val strategy: String? = null, - val confidenceBucket: String? = null, - val countBucket: String? = null, - val candidateCountBucket: String? = null, - val durationMsBucket: String? = null, - val rowsAddedBucket: String? = null, - val statusCodeBucket: String? = null, - val sdkIntBucket: String? = null, - val source: String? = null, - val persisted: String? = null, - val granted: Boolean? = null, - val retried: Boolean? = null, -) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt index ccbc93cd5..c9088f6e2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt @@ -36,7 +36,6 @@ import zed.rainxch.core.data.dto.AnnouncementsResponseDto import zed.rainxch.core.data.dto.BackendExploreResponse import zed.rainxch.core.data.dto.BackendRepoResponse import zed.rainxch.core.data.dto.BackendSearchResponse -import zed.rainxch.core.data.dto.EventRequest import zed.rainxch.core.data.dto.ExternalMatchRequest import zed.rainxch.core.data.dto.ExternalMatchResponse import zed.rainxch.core.data.dto.GithubReadmeResponseDto @@ -381,22 +380,6 @@ class BackendApiClient( } } - suspend fun postEvents(events: List): Result = - safeCall { - val response = httpClient.post("events") { - contentType(ContentType.Application.Json) - setBody(events) - } - when { - response.status == HttpStatusCode.NoContent || response.status.isSuccess() -> - Result.success(Unit) - response.status == HttpStatusCode.TooManyRequests -> - Result.failure(RateLimitedException()) - else -> - Result.failure(BackendException(response.status.value)) - } - } - private suspend fun buildRateLimited(response: io.ktor.client.statement.HttpResponse): RateLimitedException { BackendRateLimitTracker.record(response) val headerRetryAfter = response.headers["Retry-After"]?.toLongOrNull() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt index c42c34937..678f04032 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt @@ -14,13 +14,13 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import zed.rainxch.core.data.data_source.TokenStore import zed.rainxch.core.domain.model.ProxyConfig -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.RateLimitRepository class GitHubClientProvider( private val tokenStore: TokenStore, private val rateLimitRepository: RateLimitRepository, - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, proxyConfigFlow: StateFlow, ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -31,7 +31,7 @@ class GitHubClientProvider( createGitHubHttpClient( tokenStore = tokenStore, rateLimitRepository = rateLimitRepository, - authenticationState = authenticationState, + userSessionRepository = userSessionRepository, proxyConfig = proxyConfigFlow.value, ) @@ -48,7 +48,7 @@ class GitHubClientProvider( createGitHubHttpClient( tokenStore = tokenStore, rateLimitRepository = rateLimitRepository, - authenticationState = authenticationState, + userSessionRepository = userSessionRepository, proxyConfig = proxyConfig, ) val previous = currentClient diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.kt index afcae952d..924dc2ed3 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.kt @@ -10,14 +10,13 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.util.network.UnresolvedAddressException -import kotlinx.coroutines.flow.Flow import kotlinx.serialization.json.Json import zed.rainxch.core.data.data_source.TokenStore import zed.rainxch.core.data.network.interceptor.RateLimitInterceptor import zed.rainxch.core.data.network.interceptor.UnauthorizedInterceptor import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.core.domain.model.RateLimitException -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.RateLimitRepository import java.io.IOException import kotlin.coroutines.cancellation.CancellationException @@ -27,7 +26,7 @@ expect fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient fun createGitHubHttpClient( tokenStore: TokenStore, rateLimitRepository: RateLimitRepository, - authenticationState: AuthenticationState? = null, + userSessionRepository: UserSessionRepository? = null, proxyConfig: ProxyConfig = ProxyConfig.System, ): HttpClient { val json = @@ -41,9 +40,9 @@ fun createGitHubHttpClient( this.rateLimitRepository = rateLimitRepository } - if (authenticationState != null) { + if (userSessionRepository != null) { install(UnauthorizedInterceptor) { - this.authenticationState = authenticationState + this.userSessionRepository = userSessionRepository } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/UnauthorizedInterceptor.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/UnauthorizedInterceptor.kt index 3d371c5d5..e51383704 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/UnauthorizedInterceptor.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/UnauthorizedInterceptor.kt @@ -5,13 +5,13 @@ import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.statement.HttpReceivePipeline import io.ktor.http.HttpHeaders import io.ktor.util.AttributeKey -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository class UnauthorizedInterceptor( - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, ) { class Config { - var authenticationState: AuthenticationState? = null + var userSessionRepository: UserSessionRepository? = null } companion object Plugin : HttpClientPlugin { @@ -21,8 +21,8 @@ class UnauthorizedInterceptor( override fun prepare(block: Config.() -> Unit): UnauthorizedInterceptor { val config = Config().apply(block) return UnauthorizedInterceptor( - authenticationState = - requireNotNull(config.authenticationState) { + userSessionRepository = + requireNotNull(config.userSessionRepository) { "AuthenticationState must be provided" }, ) @@ -35,9 +35,9 @@ class UnauthorizedInterceptor( scope.receivePipeline.intercept(HttpReceivePipeline.After) { val tokenKey = extractBearerToken(subject.call.request.headers[HttpHeaders.Authorization]) if (subject.status.value == 401) { - plugin.authenticationState.notifySessionExpired(tokenKey) + plugin.userSessionRepository.notifySessionExpired(tokenKey) } else { - plugin.authenticationState.notifyRequestSucceeded(tokenKey) + plugin.userSessionRepository.notifyRequestSucceeded(tokenKey) } proceedWith(subject) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt deleted file mode 100644 index ee136b5ac..000000000 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/DeviceIdentityRepositoryImpl.kt +++ /dev/null @@ -1,87 +0,0 @@ -package zed.rainxch.core.data.repository - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import eu.anifantakis.lib.ksafe.KSafe -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid -import zed.rainxch.core.domain.repository.DeviceIdentityRepository -import zed.rainxch.core.data.secure.safeGet -import zed.rainxch.core.data.secure.safePut - -@OptIn(ExperimentalUuidApi::class) -class DeviceIdentityRepositoryImpl( - private val ksafe: KSafe, - private val legacyDataStore: DataStore, -) : DeviceIdentityRepository { - - private val deviceIdMutex = Mutex() - - @Volatile private var migrated: Boolean = false - - override suspend fun getDeviceId(): String = - deviceIdMutex.withLock { - migrateIfNeeded() - // Let read failures surface — a transient Keystore / DataStore - // hiccup must not silently mint a new ID and orphan telemetry. - val existing = ksafe.safeGet(DEVICE_ID_KEY, "") - if (existing.isNotBlank()) return existing - - val generated = Uuid.random().toString() - ksafe.safePut(DEVICE_ID_KEY, generated) - generated - } - - override suspend fun resetDeviceId(): String = - deviceIdMutex.withLock { - // Migrate first so any pre-existing legacy entry is removed even - // if the user resets before the first getDeviceId() call. - migrateIfNeeded() - val next = Uuid.random().toString() - ksafe.safePut(DEVICE_ID_KEY, next) - // Reset semantics: any legacy copy must die too. - runCatching { - legacyDataStore.edit { it.remove(stringPreferencesKey("anonymous_device_id")) } - } - next - } - - private suspend fun migrateIfNeeded() { - if (migrated) return - val existing = runCatching { ksafe.safeGet(DEVICE_ID_KEY, "") }.getOrDefault("") - if (existing.isNotBlank()) { - migrated = true - return - } - val legacy = runCatching { - legacyDataStore.data.first()[stringPreferencesKey("anonymous_device_id")] - }.getOrNull() - if (legacy.isNullOrBlank()) { - // Nothing to migrate; first-run install or already cleaned. - migrated = true - return - } - val putOk = ksafe.safePut(DEVICE_ID_KEY, legacy) - if (!putOk) { - // Keep legacy intact for next attempt; do not flip marker. - return - } - val deleteResult = runCatching { - legacyDataStore.edit { it.remove(stringPreferencesKey("anonymous_device_id")) } - } - if (deleteResult.isFailure) { - // Plaintext copy still on disk — retry cleanup next launch. - return - } - migrated = true - } - - private companion object { - const val DEVICE_ID_KEY = "anonymous_device_id" - } -} diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt index 1fce12802..1b9611afe 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt @@ -28,7 +28,6 @@ import zed.rainxch.core.data.network.BackendException import zed.rainxch.core.data.network.ExternalMatchApi import zed.rainxch.core.data.network.RateLimitedException import zed.rainxch.core.domain.repository.ExternalImportRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.system.ExternalAppCandidate import zed.rainxch.core.domain.system.ExternalAppScanner import zed.rainxch.core.domain.system.ExternalDecisionSnapshot @@ -48,14 +47,9 @@ class ExternalImportRepositoryImpl( private val legacyDataStore: DataStore, private val externalMatchApi: ExternalMatchApi, private val backendClient: BackendApiClient, - private val telemetry: TelemetryRepository, private val forgejoClientRegistry: zed.rainxch.core.data.network.ForgejoClientRegistry, private val tweaksRepository: zed.rainxch.core.domain.repository.TweaksRepository, ) : ExternalImportRepository { - // Snapshot cache survives only for the lifetime of the process. Decisions - // (linked / skipped / never-ask) are persisted in `external_links`; the - // raw candidate metadata (label, fingerprint, hint) is regenerated on the - // next scan rather than persisted to keep the schema small. private val candidateSnapshot = MutableStateFlow>(emptyMap()) @@ -76,10 +70,6 @@ class ExternalImportRepositoryImpl( ksafe.safeGet(K_INITIAL_SCAN_AT, null) }.getOrNull() == null runCatching { - if (firstLaunch) { - runCatching { telemetry.importScanStarted(trigger = "first_launch") } - .onFailure { Logger.d { "telemetry importScanStarted failed: ${it.message}" } } - } runFullScan() }.onSuccess { if (firstLaunch) markInitialScanComplete() @@ -119,12 +109,6 @@ class ExternalImportRepositoryImpl( .onFailure { Logger.d { "prune pending failed: ${it.message}" } } val durationMs = nowMillis() - started - runCatching { - telemetry.importScanCompleted( - candidateCountBucket = bucketCandidateCount(candidates.size), - durationMsBucket = bucketDurationMs(durationMs), - ) - }.onFailure { Logger.d { "telemetry importScanCompleted failed: ${it.message}" } } return ScanResult( totalCandidates = candidates.size, @@ -232,12 +216,6 @@ class ExternalImportRepositoryImpl( } }.onFailure { error -> Logger.w(error) { "external-match batch failed; continuing" } - runCatching { - telemetry.externalMatchApiFailure( - statusCodeBucket = bucketApiFailure(error), - retried = false, - ) - }.onFailure { Logger.d { "telemetry externalMatchApiFailure failed: ${it.message}" } } } } @@ -347,19 +325,6 @@ class ExternalImportRepositoryImpl( .distinctBy { "${it.sourceHost ?: "github"}|${it.owner}/${it.repo}" } .sortedByDescending { it.confidence } - // Emit one `import_match_attempted` per strategy that - // produced a hit for this candidate. Bucketed confidence - // only — never owner/repo/package name. - deduped.groupBy { it.source }.forEach { (source, hits) -> - val top = hits.maxByOrNull { it.confidence } ?: return@forEach - runCatching { - telemetry.importMatchAttempted( - strategy = source.telemetryStrategy(), - confidenceBucket = bucketConfidence(top.confidence), - ) - }.onFailure { Logger.d { "telemetry importMatchAttempted failed: ${it.message}" } } - } - RepoMatchResult(packageName = candidate.packageName, suggestions = deduped) } } @@ -551,16 +516,6 @@ class ExternalImportRepositoryImpl( } catch (e: Exception) { Logger.w(e) { "signing-seeds sync aborted" } } - emitSeedSyncTelemetry(rowsAdded, nowMillis() - started) - } - - private suspend fun emitSeedSyncTelemetry(rowsAdded: Int, durationMs: Long) { - runCatching { - telemetry.signingSeedSyncCompleted( - rowsAddedBucket = bucketSeedRowsAdded(rowsAdded), - durationMsBucket = bucketDurationMs(durationMs), - ) - }.onFailure { Logger.d { "telemetry signingSeedSyncCompleted failed: ${it.message}" } } } override suspend fun pruneExpiredSkips() { @@ -745,15 +700,6 @@ class ExternalImportRepositoryImpl( } } - private fun RepoMatchSource.telemetryStrategy(): String = - when (this) { - RepoMatchSource.MANIFEST -> "manifest" - RepoMatchSource.SEARCH -> "search" - RepoMatchSource.FINGERPRINT -> "fingerprint" - RepoMatchSource.MANUAL -> "manual" - RepoMatchSource.FORGEJO_SEARCH -> "forgejo_search" - } - companion object { private const val K_INITIAL_SCAN_AT = "external_import_initial_scan_at" private const val SKIP_TTL_MILLIS: Long = 7L * 24 * 60 * 60 * 1000 diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt deleted file mode 100644 index 9a720a33f..000000000 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TelemetryRepositoryImpl.kt +++ /dev/null @@ -1,335 +0,0 @@ -package zed.rainxch.core.data.repository - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import zed.rainxch.core.data.BuildKonfig -import zed.rainxch.core.data.dto.EventRequest -import zed.rainxch.core.data.network.BackendApiClient -import zed.rainxch.core.domain.logging.GitHubStoreLogger -import zed.rainxch.core.domain.model.Platform -import zed.rainxch.core.domain.repository.DeviceIdentityRepository -import zed.rainxch.core.domain.repository.TelemetryRepository -import zed.rainxch.core.domain.repository.TweaksRepository - -class TelemetryRepositoryImpl( - private val backendApiClient: BackendApiClient, - private val deviceIdentity: DeviceIdentityRepository, - private val tweaksRepository: TweaksRepository, - private val platform: Platform, - private val appScope: CoroutineScope, - private val logger: GitHubStoreLogger, -) : TelemetryRepository { - - private val bufferMutex = Mutex() - private val buffer = ArrayDeque() - - init { - appScope.launch { - while (true) { - delay(FLUSH_INTERVAL_MS) - runCatching { flushPending() } - .onFailure { logger.debug("Telemetry flush error: ${it.message}") } - } - } - } - - // ── recording (fire-and-forget, guarded by opt-in) ────────────── - - override fun recordSearchPerformed(resultCount: Int) { - // Query intentionally dropped: a 16-hex SHA-256 prefix of a - // lowercased repo-name search is rainbow-table-trivial. The - // count-only signal here is what survives. - enqueue( - eventType = "search_performed", - resultCount = resultCount, - ) - } - - override fun recordSearchResultClicked(repoId: Long) { - enqueue(eventType = "search_result_clicked", repoId = repoId) - } - - override fun recordRepoViewed(repoId: Long) { - enqueue(eventType = "repo_viewed", repoId = repoId) - } - - override fun recordReleaseDownloaded(repoId: Long) { - enqueue(eventType = "release_downloaded", repoId = repoId) - } - - override fun recordInstallStarted(repoId: Long) { - enqueue(eventType = "install_started", repoId = repoId) - } - - override fun recordInstallSucceeded(repoId: Long) { - enqueue(eventType = "install_succeeded", repoId = repoId, success = true) - } - - override fun recordInstallFailed(repoId: Long, errorCode: String?) { - enqueue( - eventType = "install_failed", - repoId = repoId, - success = false, - errorCode = errorCode, - ) - } - - override fun recordAppOpenedAfterInstall(repoId: Long) { - enqueue(eventType = "app_opened_after_install", repoId = repoId) - } - - override fun recordUninstalled(repoId: Long) { - enqueue(eventType = "uninstalled", repoId = repoId) - } - - override fun recordFavorited(repoId: Long) { - enqueue(eventType = "favorited", repoId = repoId) - } - - override fun recordUnfavorited(repoId: Long) { - enqueue(eventType = "unfavorited", repoId = repoId) - } - - // ── E1 external-import events ─────────────────────────────────── - // Privacy invariant: never pass package names, repo names, app - // labels, or signing fingerprints — only bucketed strings, enums, - // and counts. Enforced in CI by `PrivacyAuditTest` (E6). - - override suspend fun importScanStarted(trigger: String) { - enqueueExt(eventType = "import_scan_started", trigger = trigger) - } - - override suspend fun importScanCompleted( - candidateCountBucket: String, - durationMsBucket: String, - ) { - enqueueExt( - eventType = "import_scan_completed", - candidateCountBucket = candidateCountBucket, - durationMsBucket = durationMsBucket, - ) - } - - override suspend fun importMatchAttempted(strategy: String, confidenceBucket: String) { - enqueueExt( - eventType = "import_match_attempted", - strategy = strategy, - confidenceBucket = confidenceBucket, - ) - } - - override suspend fun importAutoLinked(countBucket: String) { - enqueueExt(eventType = "import_auto_linked", countBucket = countBucket) - } - - override suspend fun importManuallyLinked(countBucket: String, source: String) { - enqueueExt( - eventType = "import_manually_linked", - countBucket = countBucket, - source = source, - ) - } - - override suspend fun importSkipped(countBucket: String, persisted: String) { - enqueueExt( - eventType = "import_skipped", - countBucket = countBucket, - persisted = persisted, - ) - } - - override suspend fun importUnlinkedFromDetails() { - enqueueExt(eventType = "import_unlinked_from_details") - } - - override suspend fun importPermissionRequested() { - enqueueExt(eventType = "import_permission_requested") - } - - override suspend fun importPermissionOutcome(granted: Boolean, sdkIntBucket: String) { - enqueueExt( - eventType = "import_permission_outcome", - granted = granted, - sdkIntBucket = sdkIntBucket, - ) - } - - override suspend fun importSearchOverrideUsed() { - enqueueExt(eventType = "import_search_override_used") - } - - override suspend fun importSearchOverrideNoResults() { - enqueueExt(eventType = "import_search_override_no_results") - } - - override suspend fun signingSeedSyncCompleted( - rowsAddedBucket: String, - durationMsBucket: String, - ) { - enqueueExt( - eventType = "signing_seed_sync_completed", - rowsAddedBucket = rowsAddedBucket, - durationMsBucket = durationMsBucket, - ) - } - - override suspend fun externalMatchApiFailure(statusCodeBucket: String, retried: Boolean) { - enqueueExt( - eventType = "external_match_api_failure", - statusCodeBucket = statusCodeBucket, - retried = retried, - ) - } - - // ── batching ──────────────────────────────────────────────────── - - override suspend fun flushPending() { - // Re-check consent: the user may have disabled telemetry between - // when these events were enqueued and now. Respect the current - // setting — withdrawn consent means the buffered events must - // never leave the device. - if (!telemetryEnabled()) { - bufferMutex.withLock { buffer.clear() } - return - } - - val pending = bufferMutex.withLock { - if (buffer.isEmpty()) return - val take = minOf(buffer.size, MAX_BATCH_SIZE) - val batch = (0 until take).map { buffer.removeFirst() } - batch - } - - val result = withContext(Dispatchers.IO) { - backendApiClient.postEvents(pending) - } - - if (result.isFailure) { - // Put events back at the front for retry next tick (bounded). - // If consent was revoked during the round-trip, drop them - // instead — the flight was already in-progress under the old - // consent, but re-adding would leak past the withdrawal. - if (telemetryEnabled()) { - bufferMutex.withLock { - for (i in pending.indices.reversed()) { - if (buffer.size < MAX_BUFFER_SIZE) buffer.addFirst(pending[i]) - } - } - } else { - bufferMutex.withLock { buffer.clear() } - } - logger.debug("Telemetry batch failed: ${result.exceptionOrNull()?.message}") - } - } - - override suspend fun clearPending() { - bufferMutex.withLock { buffer.clear() } - } - - private suspend fun telemetryEnabled(): Boolean = - runCatching { tweaksRepository.getTelemetryEnabled().first() } - .getOrDefault(false) - - // ── helpers ───────────────────────────────────────────────────── - - private fun enqueue( - eventType: String, - repoId: Long? = null, - resultCount: Int? = null, - success: Boolean? = null, - errorCode: String? = null, - ) { - appScope.launch { - if (!telemetryEnabled()) return@launch - - val deviceId = runCatching { deviceIdentity.getDeviceId() }.getOrNull() ?: return@launch - - val event = EventRequest( - deviceId = deviceId, - platform = platformSlug(), - appVersion = BuildKonfig.VERSION_NAME, - eventType = eventType, - repoId = repoId, - resultCount = resultCount, - success = success, - errorCode = errorCode, - ) - - bufferMutex.withLock { - if (buffer.size >= MAX_BUFFER_SIZE) { - buffer.removeFirst() - } - buffer.add(event) - } - } - } - - private fun enqueueExt( - eventType: String, - trigger: String? = null, - strategy: String? = null, - confidenceBucket: String? = null, - countBucket: String? = null, - candidateCountBucket: String? = null, - durationMsBucket: String? = null, - rowsAddedBucket: String? = null, - statusCodeBucket: String? = null, - sdkIntBucket: String? = null, - source: String? = null, - persisted: String? = null, - granted: Boolean? = null, - retried: Boolean? = null, - ) { - appScope.launch { - if (!telemetryEnabled()) return@launch - - val deviceId = runCatching { deviceIdentity.getDeviceId() }.getOrNull() ?: return@launch - - val event = EventRequest( - deviceId = deviceId, - platform = platformSlug(), - appVersion = BuildKonfig.VERSION_NAME, - eventType = eventType, - trigger = trigger, - strategy = strategy, - confidenceBucket = confidenceBucket, - countBucket = countBucket, - candidateCountBucket = candidateCountBucket, - durationMsBucket = durationMsBucket, - rowsAddedBucket = rowsAddedBucket, - statusCodeBucket = statusCodeBucket, - sdkIntBucket = sdkIntBucket, - source = source, - persisted = persisted, - granted = granted, - retried = retried, - ) - - bufferMutex.withLock { - if (buffer.size >= MAX_BUFFER_SIZE) { - buffer.removeFirst() - } - buffer.add(event) - } - } - } - - private fun platformSlug(): String = when (platform) { - Platform.ANDROID -> "android" - Platform.MACOS -> "desktop-macos" - Platform.WINDOWS -> "desktop-windows" - Platform.LINUX -> "desktop-linux" - } - - private companion object { - private const val FLUSH_INTERVAL_MS = 30_000L - private const val MAX_BATCH_SIZE = 50 - private const val MAX_BUFFER_SIZE = 500 - } -} diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index f27f560f4..2e3a15adf 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -216,9 +216,6 @@ class TweaksRepositoryImpl( ksafe.safePut(K_CONTENT_WIDTH, width.name) } - override fun getTelemetryEnabled(): Flow = gatedGetFlow(K_TELEMETRY_ENABLED, false) - override suspend fun setTelemetryEnabled(enabled: Boolean) { migrationDeferred.await(); ksafe.safePut(K_TELEMETRY_ENABLED, enabled) } - override fun getTranslationProvider(): Flow = gatedGetFlow(K_TRANSLATION_PROVIDER, "").map { TranslationProvider.fromName(it.ifEmpty { null }) } @@ -416,7 +413,6 @@ class TweaksRepositoryImpl( MigrationEntry(booleanPreferencesKey("include_pre_releases"), K_INCLUDE_PRE_RELEASES), MigrationEntry(booleanPreferencesKey("hide_seen_enabled"), K_HIDE_SEEN_ENABLED), MigrationEntry(booleanPreferencesKey("scrollbar_enabled"), K_SCROLLBAR_ENABLED), - MigrationEntry(booleanPreferencesKey("telemetry_enabled"), K_TELEMETRY_ENABLED), MigrationEntry(stringPreferencesKey("translation_provider"), K_TRANSLATION_PROVIDER), MigrationEntry(stringPreferencesKey("youdao_app_key"), K_YOUDAO_APP_KEY), MigrationEntry(stringPreferencesKey("youdao_app_secret"), K_YOUDAO_APP_SECRET), @@ -486,7 +482,6 @@ class TweaksRepositoryImpl( private const val K_INCLUDE_PRE_RELEASES = "include_pre_releases" private const val K_HIDE_SEEN_ENABLED = "hide_seen_enabled" private const val K_SCROLLBAR_ENABLED = "scrollbar_enabled" - private const val K_TELEMETRY_ENABLED = "telemetry_enabled" private const val K_TRANSLATION_PROVIDER = "translation_provider" private const val K_YOUDAO_APP_KEY = "youdao_app_key" private const val K_YOUDAO_APP_SECRET = "youdao_app_secret" diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AuthenticationStateImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt similarity index 96% rename from core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AuthenticationStateImpl.kt rename to core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt index d514633a6..ba6e752bf 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AuthenticationStateImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt @@ -9,14 +9,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import zed.rainxch.core.data.data_source.TokenStore -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import kotlin.time.Clock import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) -class AuthenticationStateImpl( +class UserSessionRepositoryImpl( private val tokenStore: TokenStore, -) : AuthenticationState { +) : UserSessionRepository { private val _sessionExpiredEvent = MutableSharedFlow(extraBufferCapacity = 1) override val sessionExpiredEvent: SharedFlow = _sessionExpiredEvent.asSharedFlow() diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt deleted file mode 100644 index 0c2fa8e2c..000000000 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/DeviceIdentityRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package zed.rainxch.core.domain.repository - -interface DeviceIdentityRepository { - suspend fun getDeviceId(): String - - suspend fun resetDeviceId(): String -} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt index d8f791c6e..f1c81c584 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt @@ -11,11 +11,6 @@ interface SeenReposRepository { suspend fun markAsSeen(repo: GithubRepoSummary) - /** - * Primitive-arg overload for surfaces that only hold the UI model - * (Home / Search cards) and would otherwise have to reconstitute a - * full [GithubRepoSummary] just to mark a repo seen. - */ suspend fun markAsSeen( repoId: Long, repoName: String, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt deleted file mode 100644 index a1d7a305b..000000000 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TelemetryRepository.kt +++ /dev/null @@ -1,64 +0,0 @@ -package zed.rainxch.core.domain.repository - -interface TelemetryRepository { - fun recordSearchPerformed(resultCount: Int) - - fun recordSearchResultClicked(repoId: Long) - - fun recordRepoViewed(repoId: Long) - - fun recordReleaseDownloaded(repoId: Long) - - fun recordInstallStarted(repoId: Long) - - fun recordInstallSucceeded(repoId: Long) - - fun recordInstallFailed(repoId: Long, errorCode: String?) - - fun recordAppOpenedAfterInstall(repoId: Long) - - fun recordUninstalled(repoId: Long) - - fun recordFavorited(repoId: Long) - - fun recordUnfavorited(repoId: Long) - - // ── E1 external-import telemetry ──────────────────────────────── - // All payloads are bucketed/enum strings — never package names, - // repo names, app labels, or fingerprints. See E1 plan §8. - - suspend fun importScanStarted(trigger: String) - - suspend fun importScanCompleted(candidateCountBucket: String, durationMsBucket: String) - - suspend fun importMatchAttempted(strategy: String, confidenceBucket: String) - - suspend fun importAutoLinked(countBucket: String) - - suspend fun importManuallyLinked(countBucket: String, source: String) - - suspend fun importSkipped(countBucket: String, persisted: String) - - suspend fun importUnlinkedFromDetails() - - suspend fun importPermissionRequested() - - suspend fun importPermissionOutcome(granted: Boolean, sdkIntBucket: String) - - suspend fun importSearchOverrideUsed() - - suspend fun importSearchOverrideNoResults() - - suspend fun signingSeedSyncCompleted(rowsAddedBucket: String, durationMsBucket: String) - - suspend fun externalMatchApiFailure(statusCodeBucket: String, retried: Boolean) - - suspend fun flushPending() - - /** - * Drops any buffered events that have not yet been transmitted. - * Called when the user resets their analytics ID so events that - * were recorded under the old ID don't leak out attached to it. - */ - suspend fun clearPending() -} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 33c4d2efd..85153c8fd 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -79,10 +79,6 @@ interface TweaksRepository { suspend fun setContentWidth(width: ContentWidth) - fun getTelemetryEnabled(): Flow - - suspend fun setTelemetryEnabled(enabled: Boolean) - fun getTranslationProvider(): Flow suspend fun setTranslationProvider(provider: TranslationProvider) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AuthenticationState.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt similarity index 91% rename from core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AuthenticationState.kt rename to core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt index 8e2803b84..e3066f1b6 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AuthenticationState.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt @@ -3,7 +3,7 @@ package zed.rainxch.core.domain.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow -interface AuthenticationState { +interface UserSessionRepository { fun isUserLoggedIn(): Flow suspend fun isCurrentlyUserLoggedIn(): Boolean diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/WhatsNewLoader.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/WhatsNewLoader.kt index 1afbcffc8..16ed4119b 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/WhatsNewLoader.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/WhatsNewLoader.kt @@ -3,17 +3,6 @@ package zed.rainxch.core.domain.repository import zed.rainxch.core.domain.model.WhatsNewEntry interface WhatsNewLoader { - /** - * Optional explicit BCP-47 [languageTag] (e.g. `"zh-CN"`, `"fr"`, - * `null`). When non-null, this takes precedence over the global - * `LocalizationManager` lookup — used to defeat the race where the - * `getAppLanguage()` flow fans out to multiple subscribers and the - * loader runs before the global `Locale.getDefault()` has caught up - * with the user's just-picked language. Null falls back to - * whatever `LocalizationManager.getCurrentLanguageCode()` returns - * at call time (suitable for paths where the tag isn't known - * upfront, e.g. a deep-linked one-shot load). - */ suspend fun loadAll(languageTag: String? = null): List suspend fun forVersionCode(versionCode: Int, languageTag: String? = null): WhatsNewEntry? diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index b49bf756c..23d3015dd 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -628,12 +628,6 @@ إخفاء المستودع تم إخفاء المستودع تراجع - الخصوصية - ساعد في تحسين البحث - مشاركة بيانات الاستخدام (عمليات البحث والتثبيتات والتفاعلات) المرتبطة بمعرّف تحليلي قابل لإعادة التعيين. لا تتم مشاركة تفاصيل الحساب. - إعادة تعيين معرف التحليلات - إنشاء معرف مجهول جديد، مما يقطع الصلة بالبيانات السابقة. - تم إعادة تعيين معرف التحليلات تمت مشاهدته مؤخرًا المستودعات التي قمت بزيارتها diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 9fce95c6b..3af1e866d 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -627,12 +627,6 @@ রিপোজিটরি লুকান রিপোজিটরি লুকানো হয়েছে পূর্বাবস্থা - গোপনীয়তা - অনুসন্ধান উন্নত করতে সহায়তা করুন - পুনরায় সেট করা যায় এমন একটি বিশ্লেষণ আইডির সাথে যুক্ত ব্যবহার ডেটা (অনুসন্ধান, ইনস্টল, ইন্টারঅ্যাকশন) শেয়ার করুন। অ্যাকাউন্ট বিবরণ শেয়ার করা হয় না। - বিশ্লেষণ আইডি রিসেট করুন - একটি নতুন বেনামী আইডি তৈরি করুন, অতীতের টেলিমেট্রির সাথে সংযোগ বিচ্ছিন্ন করে। - বিশ্লেষণ আইডি রিসেট করা হয়েছে সাম্প্রতিক দেখা আপনি যেসব রিপোজিটরি দেখেছেন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 6271addb5..c69ed96e0 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -588,12 +588,6 @@ Ocultar repositorio Repositorio ocultado Deshacer - Privacidad - Ayudar a mejorar la búsqueda - Compartir datos de uso (búsquedas, instalaciones, interacciones) asociados a un ID de análisis restablecible. No se comparten detalles de la cuenta. - Restablecer ID de análisis - Generar un nuevo ID anónimo, cortando el vínculo con la telemetría anterior. - ID de análisis restablecido Vistos recientemente Repositorios que has visitado diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 1910b3fa1..5bb81341b 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -589,12 +589,6 @@ Masquer le dépôt Dépôt masqué Annuler - Confidentialité - Aider à améliorer la recherche - Partager des données d'utilisation (recherches, installations, interactions) associées à un identifiant d'analyse réinitialisable. Aucun détail de compte n'est partagé. - Réinitialiser l'ID d'analytique - Générer un nouvel ID anonyme, coupant le lien avec la télémétrie passée. - ID d'analytique réinitialisé Récemment consultés Dépôts que vous avez visités diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 69c14f331..0bbdb7b13 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -626,12 +626,6 @@ रिपॉजिटरी छिपाएं रिपॉजिटरी छिपाई गई पूर्ववत् - गोपनीयता - खोज को बेहतर बनाने में मदद करें - रीसेट करने योग्य एनालिटिक्स आईडी से जुड़े उपयोग डेटा (खोज, इंस्टॉल, इंटरैक्शन) साझा करें। खाता विवरण साझा नहीं किए जाते। - एनालिटिक्स आईडी रीसेट करें - एक नया अनाम आईडी बनाएं, पिछले टेलीमेट्री से लिंक को तोड़ें। - एनालिटिक्स आईडी रीसेट किया गया हाल ही में देखा गया वे रिपॉजिटरी जिन्हें आपने देखा है diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 92ddf6513..57bbf7d41 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -627,12 +627,6 @@ Nascondi repository Repository nascosto Annulla - Privacy - Aiuta a migliorare la ricerca - Condividi dati di utilizzo (ricerche, installazioni, interazioni) associati a un ID analitico ripristinabile. Nessun dettaglio dell'account viene condiviso. - Reimposta ID analitico - Genera un nuovo ID anonimo, interrompendo il collegamento con la telemetria passata. - ID analitico reimpostato Visualizzati di recente Repository che hai visitato diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 5f3af264e..116df7ad7 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -590,12 +590,6 @@ リポジトリを非表示 リポジトリを非表示にしました 元に戻す - プライバシー - 検索の改善に協力 - リセット可能な分析 ID に関連付けられた使用データ(検索、インストール、操作)を共有します。アカウント情報は共有されません。 - 分析IDをリセット - 新しい匿名IDを生成し、過去のテレメトリとのリンクを切断します。 - 分析IDをリセットしました 最近閲覧した 訪問したリポジトリ diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 003cae28a..ac7e0fedd 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -625,12 +625,6 @@ 저장소 숨기기 저장소가 숨겨졌습니다 실행 취소 - 개인 정보 보호 - 검색 개선에 도움 주기 - 재설정 가능한 분석 ID에 연결된 사용 데이터(검색, 설치, 상호작용)를 공유합니다. 계정 정보는 공유되지 않습니다. - 분석 ID 재설정 - 새 익명 ID를 생성하여 과거 원격 측정과의 연결을 끊습니다. - 분석 ID가 재설정되었습니다 최근 본 항목 방문한 저장소 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 9ab37f9a3..cc6146f9a 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -591,12 +591,6 @@ Ukryj repozytorium Repozytorium ukryte Cofnij - Prywatność - Pomóż ulepszyć wyszukiwanie - Udostępniaj dane o użyciu (wyszukiwania, instalacje, interakcje) powiązane z resetowalnym identyfikatorem analitycznym. Żadne dane konta nie są udostępniane. - Zresetuj ID analityki - Wygeneruj nowy anonimowy ID, zrywając powiązanie z poprzednią telemetrią. - ID analityki zresetowany Ostatnio oglądane Repozytoria, które odwiedziłeś diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 964d62b6f..e1d7f103b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -591,12 +591,6 @@ Скрыть репозиторий Репозиторий скрыт Отменить - Конфиденциальность - Помочь улучшить поиск - Отправлять данные об использовании (поиски, установки, взаимодействия), связанные со сбрасываемым идентификатором аналитики. Данные учётной записи не передаются. - Сбросить ID аналитики - Сгенерировать новый анонимный ID, разорвав связь с прошлой телеметрией. - ID аналитики сброшен Недавно просмотренные Репозитории, которые вы посещали diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 38c6889ca..769d2de35 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -625,12 +625,6 @@ Depoyu gizle Depo gizlendi Geri al - Gizlilik - Aramayı iyileştirmeye yardım et - Sıfırlanabilir bir analiz kimliğiyle ilişkilendirilmiş kullanım verilerini (aramalar, yüklemeler, etkileşimler) paylaş. Hesap ayrıntıları paylaşılmaz. - Analitik kimliğini sıfırla - Yeni anonim kimlik oluştur, geçmiş telemetri ile bağı kopar. - Analitik kimliği sıfırlandı Son görüntülenenler Ziyaret ettiğiniz depolar diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index f8a647e26..03cc7d2c4 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -591,12 +591,6 @@ 隐藏仓库 仓库已隐藏 撤销 - 隐私 - 帮助改进搜索 - 分享与可重置分析 ID 关联的使用数据(搜索、安装、交互)。不分享账户详情。 - 重置分析 ID - 生成新的匿名 ID,切断与过去遥测数据的联系。 - 分析 ID 已重置 最近查看 你访问过的仓库 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 55ed1e3b0..8ac3f8a56 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -793,12 +793,6 @@ Could not unhide. Try again. - Privacy - Help improve search - Share usage data (searches, installs, interactions) associated with a resettable analytics ID. No account details are shared. - Reset analytics ID - Generate a new anonymous ID, severing the link to past telemetry. - Analytics ID reset Recent searches diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt index 548a5ec34..2989a1d36 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt @@ -28,7 +28,6 @@ import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.DeviceApp import zed.rainxch.core.domain.repository.ExternalImportRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.system.ExternalAppCandidate import zed.rainxch.core.domain.system.ExternalDecisionSnapshot import zed.rainxch.core.domain.system.InstallerKind @@ -54,7 +53,6 @@ class ExternalImportViewModel( private val externalImportRepository: ExternalImportRepository, private val appsRepository: AppsRepository, private val installedAppsRepository: InstalledAppsRepository, - private val telemetry: TelemetryRepository, private val logger: GitHubStoreLogger, private val tweaksRepository: zed.rainxch.core.domain.repository.TweaksRepository, ) : ViewModel() { @@ -103,7 +101,6 @@ class ExternalImportViewModel( ExternalImportAction.OnRequestPermission -> { _state.update { it.copy(phase = ImportPhase.RequestingPermission) } - viewModelScope.launch { runCatching { telemetry.importPermissionRequested() } } } is ExternalImportAction.OnPermissionGranted -> { @@ -436,12 +433,10 @@ class ExternalImportViewModel( ), ) } - runCatching { telemetry.importSearchOverrideUsed() } return@launch } // Not a URL — backend free-text search. - runCatching { telemetry.importSearchOverrideUsed() } val result = runCatching { externalImportRepository.searchRepos(query) } .getOrElse { e -> if (e is CancellationException) throw e @@ -450,7 +445,6 @@ class ExternalImportViewModel( result.fold( onSuccess = { suggestions -> if (suggestions.isEmpty()) { - runCatching { telemetry.importSearchOverrideNoResults() } } _state.update { if (it.activeSearchPackage != packageName) it @@ -521,12 +515,6 @@ class ExternalImportViewModel( return@launch } - runCatching { - telemetry.importSkipped( - countBucket = "1-2", - persisted = if (neverAsk) "forever" else "7day", - ) - } removeCardFromState(packageName) { it.copy(skipped = it.skipped + 1) } pendingUndo = PendingUndo( @@ -590,9 +578,6 @@ class ExternalImportViewModel( ) return@launch } - runCatching { - telemetry.importManuallyLinked(countBucket = "1-2", source = source) - } removeCardFromState(packageName) { it.copy(manuallyLinked = it.manuallyLinked + 1) } pendingUndo = PendingUndo( @@ -779,14 +764,8 @@ class ExternalImportViewModel( } private fun emitPermissionOutcome(granted: Boolean, sdkInt: Int?) { - viewModelScope.launch { - runCatching { - telemetry.importPermissionOutcome( - granted = granted, - sdkIntBucket = bucketSdkInt(sdkInt), - ) - } - } + // Telemetry removed — kept signature so existing call sites compile; + // safe to drop entirely once those are migrated. } private fun bucketSdkInt(sdkInt: Int?): String = @@ -831,15 +810,6 @@ class ExternalImportViewModel( } } - if (successes.isNotEmpty()) { - runCatching { - telemetry.importSkipped( - countBucket = bucketCount(successes.size), - persisted = "7day", - ) - } - } - // Skip-remaining is intentionally not undoable — bulk skip clears // the wizard and triggers the completion screen, and a single // snackbar isn't a sensible affordance for "undo seven things". diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt index ebee4e618..e094885b1 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt @@ -13,12 +13,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import zed.rainxch.apps.domain.repository.AppsRepository import zed.rainxch.core.domain.model.RateLimitException -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.StarredRepository class StarredPickerViewModel( - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, private val starredRepository: StarredRepository, private val installedAppsRepository: InstalledAppsRepository, private val appsRepository: AppsRepository, @@ -73,7 +73,7 @@ class StarredPickerViewModel( private fun bootstrap() { scanJob?.cancel() scanJob = viewModelScope.launch { - val isAuthenticated = authenticationState.isCurrentlyUserLoggedIn() + val isAuthenticated = userSessionRepository.isCurrentlyUserLoggedIn() _state.update { it.copy( phase = StarredPickerState.Phase.LoadingStars, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index fc5d39ee6..abe7d4ffe 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -41,7 +41,6 @@ import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.ApkInspector import zed.rainxch.core.domain.system.DownloadOrchestrator @@ -129,10 +128,9 @@ class DetailsViewModel( private val installationManager: InstallationManager, private val attestationVerifier: AttestationVerifier, private val downloadOrchestrator: DownloadOrchestrator, - private val telemetryRepository: TelemetryRepository, private val externalImportRepository: ExternalImportRepository, private val apkInspector: ApkInspector, - private val authenticationState: zed.rainxch.core.domain.repository.AuthenticationState, + private val userSessionRepository: zed.rainxch.core.domain.repository.UserSessionRepository, private val systemInstallSerializer: zed.rainxch.core.domain.system.SystemInstallSerializer, private val profileRepository: zed.rainxch.profile.domain.repository.ProfileRepository, ) : ViewModel() { @@ -173,7 +171,6 @@ class DetailsViewModel( viewModelScope.launch { try { installer.uninstall(installedApp.packageName) - _state.value.repository?.id?.let { telemetryRepository.recordUninstalled(it) } } catch (e: Exception) { logger.error("Failed to request uninstall for ${installedApp.packageName}: ${e.message}") _events.send( @@ -199,7 +196,6 @@ class DetailsViewModel( externalImportRepository.unlink(packageName) installedAppsRepository.deleteInstalledApp(packageName) } - runCatching { telemetryRepository.importUnlinkedFromDetails() } _events.send( DetailsEvent.OnMessage( getString(Res.string.details_unlink_external_app_success), @@ -1396,7 +1392,6 @@ class DetailsViewModel( val installedApp = _state.value.installedApp ?: return val launched = installer.openApp(installedApp.packageName) if (launched && platform == Platform.ANDROID) { - _state.value.repository?.id?.let { telemetryRepository.recordAppOpenedAfterInstall(it) } } if (!launched) { viewModelScope.launch { @@ -1485,9 +1480,7 @@ class DetailsViewModel( _state.value = _state.value.copy(isFavourite = newFavoriteState) if (newFavoriteState) { - telemetryRepository.recordFavorited(repo.id) } else { - telemetryRepository.recordUnfavorited(repo.id) } _events.send( @@ -1553,7 +1546,6 @@ class DetailsViewModel( viewModelScope.launch { try { installer.uninstall(installedApp.packageName) - _state.value.repository?.id?.let { telemetryRepository.recordUninstalled(it) } } catch (e: Exception) { logger.error("Failed to request uninstall for ${installedApp.packageName}: ${e.message}") _events.send( @@ -1980,8 +1972,6 @@ class DetailsViewModel( if (!telemetryStartFired) { telemetryStartFired = true _state.value.repository?.id?.let { id -> - telemetryRepository.recordReleaseDownloaded(id) - telemetryRepository.recordInstallStarted(id) } } } @@ -2007,8 +1997,6 @@ class DetailsViewModel( if (!telemetryStartFired) { telemetryStartFired = true _state.value.repository?.id?.let { id -> - telemetryRepository.recordReleaseDownloaded(id) - telemetryRepository.recordInstallStarted(id) } } // Run the existing install dialog flow on the @@ -2024,7 +2012,6 @@ class DetailsViewModel( sizeBytes = sizeBytes, releaseTag = releaseTag, ) - _state.value.repository?.id?.let { telemetryRepository.recordInstallSucceeded(it) } // Successful install — release the entry // from the orchestrator so the apps row // doesn't keep showing "ready to install". @@ -2045,7 +2032,6 @@ class DetailsViewModel( result = Error(t.message), ) _state.value.repository?.id?.let { - telemetryRepository.recordInstallFailed(it, t.message) } } } @@ -2068,7 +2054,6 @@ class DetailsViewModel( ) if (isCompleted) { _state.value.repository?.id?.let { - telemetryRepository.recordInstallSucceeded(it) } } @@ -2145,7 +2130,6 @@ class DetailsViewModel( result = Error(entry.errorMessage), ) _state.value.repository?.id?.let { - telemetryRepository.recordInstallFailed(it, entry.errorMessage) } downloadOrchestrator.dismiss(packageKey) return@collect @@ -2786,7 +2770,6 @@ class DetailsViewModel( insights.latestStableHasInstallableAsset, ) - telemetryRepository.recordRepoViewed(repo.id) observeInstalledApp(repo.id) @@ -2797,7 +2780,7 @@ class DetailsViewModel( } catch (e: RateLimitException) { logger.error("Rate limited: ${e.message}") val seconds = e.rateLimitInfo.timeUntilReset().inWholeSeconds - val signedIn = authenticationState.isCurrentlyUserLoggedIn() + val signedIn = userSessionRepository.isCurrentlyUserLoggedIn() val base = if (seconds > 0L) { getString(Res.string.rate_limit_exceeded_retry_in, seconds.toInt()) } else { diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt index fd4ea7a79..65b15de70 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt @@ -18,13 +18,11 @@ import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.favourites.presentation.mappers.toFavouriteRepositoryUi import zed.rainxch.favourites.presentation.model.FavouritesSortRule -import zed.rainxch.profile.domain.repository.ProfileRepository import kotlin.time.Clock import kotlin.time.ExperimentalTime class FavouritesViewModel( private val favouritesRepository: FavouritesRepository, - private val profileRepository: ProfileRepository, private val tweaksRepository: TweaksRepository, ) : ViewModel() { private var hasLoadedInitialData = false diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt index 212d1c768..f285e7742 100644 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt +++ b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt @@ -8,7 +8,7 @@ val profileModule = module { single { ProfileRepositoryImpl( - authenticationState = get(), + userSessionRepository = get(), tokenStore = get(), clientProvider = get(), cacheManager = get(), diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt index 2f564e103..30139cc6a 100644 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt +++ b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt @@ -5,7 +5,6 @@ import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.http.HttpHeaders import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -17,14 +16,14 @@ import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.executeRequest import zed.rainxch.core.data.services.FileLocationsProvider import zed.rainxch.core.domain.logging.GitHubStoreLogger -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.feature.profile.data.BuildKonfig import zed.rainxch.profile.data.mappers.toUserProfile import zed.rainxch.profile.domain.model.UserProfile import zed.rainxch.profile.domain.repository.ProfileRepository class ProfileRepositoryImpl( - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, private val tokenStore: TokenStore, private val clientProvider: GitHubClientProvider, private val cacheManager: CacheManager, @@ -39,7 +38,7 @@ class ProfileRepositoryImpl( override val isUserLoggedIn: Flow get() = - authenticationState + userSessionRepository .isUserLoggedIn() .flowOn(Dispatchers.IO) diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt index 14b546eb8..6cb80e2e0 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt @@ -1,7 +1,9 @@ package zed.rainxch.search.data.di import org.koin.dsl.module +import zed.rainxch.domain.repository.SearchHistoryRepository import zed.rainxch.domain.repository.SearchRepository +import zed.rainxch.search.data.repository.SearchHistoryRepositoryImpl import zed.rainxch.search.data.repository.SearchRepositoryImpl val searchModule = @@ -14,4 +16,10 @@ val searchModule = forgejoClientRegistry = get(), ) } + + single { + SearchHistoryRepositoryImpl( + searchHistoryDao = get(), + ) + } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SearchHistoryRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchHistoryRepositoryImpl.kt similarity index 90% rename from core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SearchHistoryRepositoryImpl.kt rename to feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchHistoryRepositoryImpl.kt index f44a45507..6cf0cdb79 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SearchHistoryRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchHistoryRepositoryImpl.kt @@ -1,11 +1,11 @@ -package zed.rainxch.core.data.repository +package zed.rainxch.search.data.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlin.time.Clock import zed.rainxch.core.data.local.db.dao.SearchHistoryDao import zed.rainxch.core.data.local.db.entities.SearchHistoryEntity -import zed.rainxch.core.domain.repository.SearchHistoryRepository +import zed.rainxch.domain.repository.SearchHistoryRepository +import kotlin.time.Clock class SearchHistoryRepositoryImpl( private val searchHistoryDao: SearchHistoryDao, @@ -33,4 +33,4 @@ class SearchHistoryRepositoryImpl( override suspend fun clearAll() { searchHistoryDao.clearAll() } -} +} \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SearchHistoryRepository.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchHistoryRepository.kt similarity index 84% rename from core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SearchHistoryRepository.kt rename to feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchHistoryRepository.kt index e101c5096..763b03876 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SearchHistoryRepository.kt +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchHistoryRepository.kt @@ -1,4 +1,4 @@ -package zed.rainxch.core.domain.repository +package zed.rainxch.domain.repository import kotlinx.coroutines.flow.Flow @@ -10,4 +10,4 @@ interface SearchHistoryRepository { suspend fun removeSearch(query: String) suspend fun clearAll() -} +} \ No newline at end of file diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 945d6a1c9..e576b66bf 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -26,10 +26,9 @@ import zed.rainxch.core.domain.model.isReallyInstalled import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository -import zed.rainxch.core.domain.repository.SearchHistoryRepository +import zed.rainxch.domain.repository.SearchHistoryRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ClipboardHelper @@ -37,7 +36,6 @@ import zed.rainxch.core.domain.utils.ShareManager import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.utils.toUi import zed.rainxch.domain.repository.SearchRepository -import zed.rainxch.profile.domain.repository.ProfileRepository import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.failed_to_share_link import zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard @@ -45,7 +43,6 @@ import zed.rainxch.githubstore.core.presentation.res.no_github_link_in_clipboard import zed.rainxch.githubstore.core.presentation.res.explore_error import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded_retry_in -import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded_signin_hint import zed.rainxch.githubstore.core.presentation.res.search_failed import zed.rainxch.search.presentation.mappers.toDomain import zed.rainxch.search.presentation.model.SearchPlatformUi @@ -65,7 +62,6 @@ class SearchViewModel( private val tweaksRepository: TweaksRepository, private val seenReposRepository: SeenReposRepository, private val searchHistoryRepository: SearchHistoryRepository, - private val telemetryRepository: TelemetryRepository, private val profileRepository: ProfileRepository, private val hiddenReposRepository: HiddenReposRepository, private val initialPlatform: SearchPlatformUi? = null, @@ -76,19 +72,11 @@ class SearchViewModel( private var explorePage = 1 private var lastExploreQuery = "" - // Cached so each pagination/explore mapping doesn't re-hit - // ProfileRepository on every result page. observeCurrentUser() keeps - // it in sync with login/logout. @Volatile private var currentUserLogin: String? = null private val exploreLog = logger.withTag("SearchExplore") companion object { - // 2 covers common CJK app names that are exactly two characters - // (`B站`, `微博`, `抖音`, `美团`, …) which the previous 3-char floor - // silently rejected even when the user explicitly tapped the IME - // search action (#372). Single-character queries are still gated - // because they'd return millions of GitHub results. private const val MIN_QUERY_LENGTH = 2 } @@ -191,10 +179,6 @@ class SearchViewModel( private fun observeHiddenRepos() { viewModelScope.launch { hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> - // Track IDs only — `computeVisibleRepos` already filters - // hidden at render time. Removing from `repositories` - // would break `OnUndoHideRepository`: once the entity is - // gone there's nothing to bring back without re-fetching. _state.update { it.copy(hiddenRepoIds = ids) } } } @@ -492,12 +476,6 @@ class SearchViewModel( _state.update { it.copy(isLoading = false, isLoadingMore = false) } - - if (isInitial) { - telemetryRepository.recordSearchPerformed( - resultCount = _state.value.repositories.size, - ) - } } catch (e: RateLimitException) { logger.debug("Rate limit exceeded: ${e.message}") val seconds = e.rateLimitInfo.timeUntilReset().inWholeSeconds @@ -739,7 +717,6 @@ class SearchViewModel( } is SearchAction.OnRepositoryClick -> { - telemetryRepository.recordSearchResultClicked(action.repository.id) // Navigation handled in composable } diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt index b1c4fe288..bb4ec5962 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.core.domain.model.FavoriteRepo -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TweaksRepository @@ -30,7 +30,7 @@ import kotlin.time.Clock import kotlin.time.ExperimentalTime class StarredReposViewModel( - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, private val starredRepository: StarredRepository, private val favouritesRepository: FavouritesRepository, private val profileRepository: ProfileRepository, @@ -54,7 +54,7 @@ class StarredReposViewModel( private fun checkAuthAndLoad() { viewModelScope.launch { - val isAuthenticated = authenticationState.isCurrentlyUserLoggedIn() + val isAuthenticated = userSessionRepository.isCurrentlyUserLoggedIn() _state.update { it.copy(isAuthenticated = isAuthenticated) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index 8a06f018c..b5cb3dbcc 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -138,12 +138,6 @@ sealed interface TweaksAction { data object OnHiddenRepositoriesClick : TweaksAction - data class OnTelemetryToggled( - val enabled: Boolean, - ) : TweaksAction - - data object OnResetAnalyticsId : TweaksAction - data class OnTranslationProviderSelected( val provider: TranslationProvider, ) : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt index a9bee5694..d3556e074 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt @@ -23,8 +23,6 @@ sealed interface TweaksEvent { data object OnSeenHistoryCleared : TweaksEvent - data object OnAnalyticsIdReset : TweaksEvent - data object OnTranslationProviderSaved : TweaksEvent data object OnYoudaoCredentialsSaved : TweaksEvent diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 7c6479c70..f7712c3c6 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -117,12 +117,6 @@ fun TweaksRoot( } } - TweaksEvent.OnAnalyticsIdReset -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.analytics_id_reset)) - } - } - TweaksEvent.OnTranslationProviderSaved -> { coroutineScope.launch { snackbarState.showSnackbar(getString(Res.string.translation_provider_saved)) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 6b38f63a2..572a03174 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -38,7 +38,6 @@ data class TweaksState( val isHideSeenEnabled: Boolean = false, val isScrollbarEnabled: Boolean = false, val contentWidth: ContentWidth = ContentWidth.COMPACT, - val isTelemetryEnabled: Boolean = false, val translationProvider: TranslationProvider = TranslationProvider.Default, /** * Transient UI-only selection used when the user picks a provider diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index a1a8099b8..3538ba80c 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -25,10 +25,8 @@ import zed.rainxch.core.domain.model.TranslationProvider import zed.rainxch.core.domain.network.ProxyTestOutcome import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.network.ProxyTester -import zed.rainxch.core.domain.repository.DeviceIdentityRepository import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.SeenReposRepository -import zed.rainxch.core.domain.repository.TelemetryRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.AggressiveOemDetector import zed.rainxch.core.domain.system.InstallerStatusProvider @@ -56,8 +54,6 @@ class TweaksViewModel( private val proxyTester: ProxyTester, private val updateScheduleManager: UpdateScheduleManager, private val seenReposRepository: SeenReposRepository, - private val deviceIdentityRepository: DeviceIdentityRepository, - private val telemetryRepository: TelemetryRepository, private val logger: GitHubStoreLogger, private val aggressiveOemDetector: AggressiveOemDetector, ) : ViewModel() { @@ -106,7 +102,6 @@ class TweaksViewModel( loadHideSeenEnabled() loadScrollbarEnabled() loadContentWidth() - loadTelemetryEnabled() loadTranslationSettings() loadAppLanguage() loadAutoTranslate() @@ -462,16 +457,6 @@ class TweaksViewModel( } } - private fun loadTelemetryEnabled() { - viewModelScope.launch { - tweaksRepository.getTelemetryEnabled().collect { enabled -> - _state.update { - it.copy(isTelemetryEnabled = enabled) - } - } - } - } - private fun loadTranslationSettings() { viewModelScope.launch { tweaksRepository.getTranslationProvider().collect { provider -> @@ -921,25 +906,6 @@ class TweaksViewModel( TweaksAction.OnFeedbackDismiss -> _state.update { it.copy(isFeedbackSheetVisible = false) } - is TweaksAction.OnTelemetryToggled -> { - viewModelScope.launch { - tweaksRepository.setTelemetryEnabled(action.enabled) - } - } - - TweaksAction.OnResetAnalyticsId -> { - viewModelScope.launch { - // Clear the telemetry buffer *before* resetting the ID. - // Order matters: any buffered event still carries the - // old device ID in its EventRequest payload, so draining - // them after the reset would leak the old ID to the - // backend attached to "fresh start" identity semantics. - telemetryRepository.clearPending() - deviceIdentityRepository.resetDeviceId() - _events.send(TweaksEvent.OnAnalyticsIdReset) - } - } - is TweaksAction.OnTranslationProviderSelected -> { when (action.provider) { TranslationProvider.GOOGLE -> { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt index e67ccfb63..81e50874f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt @@ -142,64 +142,6 @@ fun LazyListScope.othersSection( }, ) - Spacer(Modifier.height(24.dp)) - - SectionHeader( - text = stringResource(Res.string.privacy_section).uppercase(), - ) - - Spacer(Modifier.height(8.dp)) - - ToggleSettingCard( - title = stringResource(Res.string.telemetry_title), - description = stringResource(Res.string.telemetry_description), - checked = state.isTelemetryEnabled, - onCheckedChange = { enabled -> - onAction(TweaksAction.OnTelemetryToggled(enabled)) - }, - ) - - Spacer(Modifier.height(8.dp)) - - ResetAnalyticsIdCard( - onClick = { - onAction(TweaksAction.OnResetAnalyticsId) - }, - ) - } -} - -@Composable -private fun ResetAnalyticsIdCard(onClick: () -> Unit) { - ExpressiveCard { - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - ) { - Text( - text = stringResource(Res.string.reset_analytics_id_title), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = stringResource(Res.string.reset_analytics_id_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt index 39fbb21a5..c00b24200 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.core.domain.model.ForgeKind import zed.rainxch.core.domain.model.HostNames -import zed.rainxch.core.domain.repository.AuthenticationState +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.HostTokenRepository import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.host_tokens_saved @@ -26,7 +26,7 @@ import zed.rainxch.githubstore.core.presentation.res.host_tokens_validation_toke class HostTokensViewModel( private val repository: HostTokenRepository, - private val authenticationState: AuthenticationState, + private val userSessionRepository: UserSessionRepository, ) : ViewModel() { private val _state = MutableStateFlow(HostTokensState(isLoading = true)) @@ -60,7 +60,7 @@ class HostTokensViewModel( } } viewModelScope.launch { - authenticationState.isUserLoggedIn() + userSessionRepository.isUserLoggedIn() .catch { /* swallow — OAuth state is non-critical for this screen */ } .collect { signedIn -> _state.update { it.copy(isOAuthSignedInToGithub = signedIn) } From d39e2f9cff15e28771f051bd8161b9b395677486 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 19:59:31 +0500 Subject: [PATCH 029/172] chore: strip comments and KDoc from all Kotlin sources --- .../zed/rainxch/githubstore/MainActivity.kt | 16 - .../rainxch/githubstore/app/GithubStoreApp.kt | 52 +-- .../OnboardingPermissions.android.kt | 14 +- .../kotlin/zed/rainxch/githubstore/Main.kt | 7 - .../app/deeplink/DeepLinkParser.kt | 24 -- .../githubstore/app/di/ViewModelsModule.kt | 9 +- .../app/navigation/BottomNavigation.kt | 13 +- .../app/navigation/BottomNavigationUtils.kt | 1 - .../app/navigation/DesktopDrawer.kt | 13 +- .../app/onboarding/OnboardingPermissions.kt | 14 - .../app/onboarding/OnboardingState.kt | 1 - .../app/whatsnew/WhatsNewViewModel.kt | 9 +- .../zed/rainxch/githubstore/A11yCrashGuard.kt | 42 +- .../zed/rainxch/githubstore/DesktopApp.kt | 35 -- .../rainxch/githubstore/DesktopDeepLink.kt | 15 - .../core/data/di/PlatformModule.android.kt | 13 - .../local/db/migrations/MIGRATION_10_11.kt | 12 - .../local/db/migrations/MIGRATION_11_12.kt | 17 - .../local/db/migrations/MIGRATION_12_13.kt | 12 - .../local/db/migrations/MIGRATION_13_14.kt | 17 - .../local/db/migrations/MIGRATION_9_10.kt | 8 - .../data/network/HttpClientFactory.android.kt | 9 +- .../services/AndroidAggressiveOemDetector.kt | 11 +- .../core/data/services/AndroidApkInspector.kt | 30 +- .../AndroidDownloadProgressNotifier.kt | 32 -- .../core/data/services/AndroidDownloader.kt | 34 +- .../services/AndroidFileLocationsProvider.kt | 6 +- .../services/AndroidInstallerInfoExtractor.kt | 20 +- .../services/AndroidLocalizationManager.kt | 6 +- .../services/AndroidPendingInstallNotifier.kt | 49 +-- .../core/data/services/AutoUpdateWorker.kt | 11 +- .../core/data/services/BootReceiver.kt | 10 +- .../data/services/DownloadCancelReceiver.kt | 18 - .../services/DownloadNotificationObserver.kt | 57 +-- .../data/services/PackageEventReceiver.kt | 138 +------ .../core/data/services/SigningFingerprint.kt | 12 +- .../core/data/services/UpdateCheckWorker.kt | 23 +- .../core/data/services/UpdateScheduler.kt | 14 +- .../dhizuku/DhizukuInstallerServiceImpl.kt | 14 +- .../services/dhizuku/DhizukuServiceManager.kt | 7 +- .../external/AndroidExternalAppScanner.kt | 14 +- .../external/InstallerSourceClassifier.kt | 6 - .../external/ManifestHintExtractor.kt | 2 - .../external/SigningFingerprintComputer.kt | 5 +- .../installer/SilentInstallerDispatcher.kt | 20 +- .../data/services/root/RootServiceManager.kt | 75 +--- .../shizuku/ShizukuInstallerServiceImpl.kt | 15 - .../core/data/utils/AndroidShareManager.kt | 4 +- .../data_source/impl/DefaultTokenStore.kt | 13 +- .../zed/rainxch/core/data/di/SharedModule.kt | 3 - .../download/MultiSourceDownloaderImpl.kt | 3 - .../data/download/SlowDownloadDetectorImpl.kt | 6 - .../zed/rainxch/core/data/dto/AssetNetwork.kt | 4 +- .../core/data/dto/GithubReadmeResponseDto.kt | 5 +- .../dto/SigningFingerprintSeedResponse.kt | 5 +- .../core/data/local/db/dao/CacheDao.kt | 7 - .../core/data/local/db/dao/FavoriteRepoDao.kt | 8 +- .../core/data/local/db/dao/InstalledAppDao.kt | 55 +-- .../core/data/local/db/dao/StarredRepoDao.kt | 8 +- .../local/db/entities/InstalledAppEntity.kt | 124 +----- .../rainxch/core/data/mappers/AssetNetwork.kt | 4 +- .../core/data/network/BackendApiClient.kt | 30 -- .../data/network/BackendFallbackPolicy.kt | 6 - .../core/data/network/ExternalMatchApi.kt | 9 +- .../core/data/network/ForgejoApiClient.kt | 25 +- .../data/network/ForgejoClientRegistry.kt | 4 - .../core/data/network/GitHubClientProvider.kt | 3 - .../core/data/network/MirrorApiClient.kt | 6 - .../data/network/MirrorRewriteInterceptor.kt | 2 - .../core/data/network/MirrorRewriter.kt | 13 - .../rainxch/core/data/network/ProxyManager.kt | 10 - .../core/data/network/ProxyManagerSeeding.kt | 8 - .../data/network/TranslationClientProvider.kt | 13 +- .../interceptor/HostTokenInterceptor.kt | 36 -- .../repository/AnnouncementsCacheStoreImpl.kt | 2 +- .../ExternalImportRepositoryImpl.kt | 161 +------- .../repository/HostTokenRepositoryImpl.kt | 11 +- .../repository/InstalledAppsRepositoryImpl.kt | 171 +------- .../data/repository/ProxyRepositoryImpl.kt | 6 +- .../data/repository/StarredRepositoryImpl.kt | 2 +- .../data/secure/DataStoreToKSafeMigrator.kt | 32 +- .../services/DefaultDownloadOrchestrator.kt | 177 --------- .../DefaultSystemInstallSerializer.kt | 9 - .../core/data/services/LocalizationManager.kt | 18 - .../core/data/di/PlatformModule.jvm.kt | 7 +- .../core/data/local/db/initDatabase.kt | 5 +- .../core/data/model/LinuxPackageType.kt | 8 +- .../data/network/HttpClientFactory.jvm.kt | 20 +- .../core/data/services/DesktopApkInspector.kt | 5 - .../DesktopDownloadProgressNotifier.kt | 5 - .../core/data/services/DesktopDownloader.kt | 11 +- .../core/data/services/DesktopInstaller.kt | 94 +---- .../services/DesktopLocalizationManager.kt | 6 +- .../services/DesktopPendingInstallNotifier.kt | 10 - .../services/DesktopUpdateScheduleManager.kt | 7 +- .../core/domain/model/ApkInspection.kt | 62 +-- .../rainxch/core/domain/model/AppLanguage.kt | 21 +- .../zed/rainxch/core/domain/model/AppTheme.kt | 10 +- .../rainxch/core/domain/model/ExportedApp.kt | 21 +- .../core/domain/model/GithubReleaseExt.kt | 32 -- .../rainxch/core/domain/model/HostToken.kt | 12 - .../rainxch/core/domain/model/InstalledApp.kt | 99 +---- .../core/domain/model/InstallerCategory.kt | 6 - .../rainxch/core/domain/model/ProxyScope.kt | 19 - .../core/domain/model/SystemArchitecture.kt | 8 +- .../rainxch/core/domain/model/ThemeMode.kt | 5 - .../core/domain/model/TranslationProvider.kt | 12 - .../core/domain/network/DigestVerifier.kt | 8 +- .../core/domain/network/ProxyTester.kt | 15 +- .../repository/InstalledAppsRepository.kt | 93 ----- .../domain/repository/MirrorRepository.kt | 12 +- .../domain/repository/TweaksRepository.kt | 45 --- .../core/domain/system/ApkInspector.kt | 20 +- .../domain/system/DownloadOrchestrator.kt | 216 +---------- .../domain/system/DownloadProgressNotifier.kt | 40 +- .../rainxch/core/domain/system/Installer.kt | 14 +- .../domain/system/PendingInstallNotifier.kt | 38 +- .../core/domain/system/RepoMatchResult.kt | 5 +- .../domain/system/SystemInstallSerializer.kt | 9 +- .../domain/system/UpdateScheduleManager.kt | 13 +- .../use_cases/SyncInstalledAppsUseCase.kt | 36 +- .../rainxch/core/domain/util/AssetFileName.kt | 83 +--- .../rainxch/core/domain/util/AssetFilter.kt | 45 +-- .../rainxch/core/domain/util/AssetPlatform.kt | 7 - .../rainxch/core/domain/util/AssetVariant.kt | 334 +--------------- .../core/domain/util/EmojiShortcodes.kt | 40 +- .../domain/util/ExternalInstallVerdict.kt | 53 +-- .../rainxch/core/domain/util/RepoIdCodec.kt | 32 +- .../core/domain/util/RepositoryUrlParser.kt | 9 +- .../domain/util/SeparateAdjacentImageLinks.kt | 53 +-- .../rainxch/core/domain/util/VersionMath.kt | 358 ++--------------- .../core/domain/util/applyThemeAwareImages.kt | 9 +- .../presentation/components/ExpressiveCard.kt | 10 +- .../presentation/components/RepositoryCard.kt | 3 +- .../components/ScrollbarContainer.kt | 8 - .../CriticalAnnouncementModal.kt | 2 +- .../components/buttons/IconButton.kt | 4 - .../components/buttons/OutlineButton.kt | 4 - .../components/buttons/PrimaryButton.kt | 5 - .../components/buttons/TintedButton.kt | 5 - .../components/cards/CompactCard.kt | 5 - .../components/cards/LeadHeroCard.kt | 6 - .../presentation/components/cards/RowCard.kt | 5 - .../components/cards/VitalSignsGrid.kt | 6 - .../components/cards/WaxSealTrustCard.kt | 6 - .../presentation/components/chips/AddChip.kt | 4 - .../components/chips/FilterChip.kt | 5 - .../components/overlays/GhsBottomSheet.kt | 7 - .../components/overlays/GhsConfirmDialog.kt | 5 - .../components/overlays/GhsDropdownMenu.kt | 4 - .../components/overlays/GhsFullScreenSheet.kt | 5 - .../components/overlays/GhsToast.kt | 6 - .../presentation/components/section/Banner.kt | 5 - .../locals/LocalScrollbarEnabled.kt | 4 - .../presentation/model/GithubRepoSummaryUi.kt | 4 +- .../rainxch/core/presentation/theme/Locals.kt | 5 - .../rainxch/core/presentation/theme/Theme.kt | 9 - .../rainxch/core/presentation/theme/Type.kt | 22 +- .../theme/shapes/WonkySquircleShape.kt | 34 +- .../core/presentation/theme/tokens/Radii.kt | 12 - .../core/presentation/theme/tokens/Schemes.kt | 20 - .../core/presentation/theme/tokens/Tokens.kt | 21 - .../core/presentation/utils/ArrowKeyScroll.kt | 7 +- .../core/presentation/utils/TimeFormatters.kt | 6 +- .../core/presentation/vocabulary/AppAccent.kt | 30 +- .../presentation/vocabulary/CookieShape.kt | 8 - .../presentation/vocabulary/DownloadWeight.kt | 4 - .../core/presentation/vocabulary/Freshness.kt | 1 - .../presentation/vocabulary/FreshnessRing.kt | 10 +- .../core/presentation/vocabulary/Heartbeat.kt | 15 +- .../presentation/vocabulary/LicensePosture.kt | 4 - .../core/presentation/vocabulary/PermDot.kt | 5 - .../presentation/vocabulary/PlatformGlyph.kt | 6 - .../presentation/vocabulary/SignalBars.kt | 4 - .../core/presentation/vocabulary/Squiggle.kt | 8 +- .../core/presentation/vocabulary/StarTier.kt | 5 - .../presentation/vocabulary/TopicGlyph.kt | 34 +- .../presentation/vocabulary/VersionDelta.kt | 6 - .../presentation/vocabulary/VersionStack.kt | 4 - .../core/presentation/vocabulary/WaxSeal.kt | 9 +- .../components/ScrollbarContainer.jvm.kt | 4 - .../utils/ApplyAndroidSystemBars.jvm.kt | 2 +- .../data/repository/AppsRepositoryImpl.kt | 14 +- .../apps/domain/repository/AppsRepository.kt | 38 +- .../components/InstalledAppIcon.android.kt | 12 +- .../PackageVisibilityRequester.android.kt | 13 +- .../util/ReducedMotionProvider.android.kt | 5 +- .../rainxch/apps/presentation/AppsAction.kt | 50 +-- .../rainxch/apps/presentation/AppsEvent.kt | 6 +- .../zed/rainxch/apps/presentation/AppsRoot.kt | 69 +--- .../rainxch/apps/presentation/AppsState.kt | 42 +- .../apps/presentation/AppsViewModel.kt | 192 +-------- .../AdvancedAppSettingsBottomSheet.kt | 24 -- .../components/AppsSectionHeader.kt | 9 - .../presentation/components/CompactAppRow.kt | 26 +- .../components/InstalledAppIcon.kt | 12 - .../components/LinkAppBottomSheet.kt | 33 +- .../components/StatusDotCluster.kt | 22 -- .../components/VariantPickerDialog.kt | 25 +- .../import/ExternalImportAction.kt | 7 - .../presentation/import/ExternalImportRoot.kt | 6 +- .../import/ExternalImportState.kt | 5 +- .../import/ExternalImportViewModel.kt | 132 ++----- .../import/components/CandidateCard.kt | 4 - .../import/components/ConfettiOverlay.kt | 5 +- .../import/components/ImportProgressScreen.kt | 4 - .../components/PermissionRationaleScreen.kt | 7 +- .../import/components/RepoCandidateRow.kt | 5 +- .../import/model/RepoSuggestionUi.kt | 4 +- .../starred/StarredPickerViewModel.kt | 2 +- .../import/util/ReducedMotionProvider.jvm.kt | 3 - .../auth/data/network/GitHubAuthApi.kt | 27 +- .../AuthenticationRepositoryImpl.kt | 25 +- .../repository/AuthenticationRepository.kt | 38 +- .../auth/presentation/AuthenticationAction.kt | 2 - .../auth/presentation/AuthenticationRoot.kt | 6 +- .../auth/presentation/AuthenticationState.kt | 7 +- .../presentation/AuthenticationViewModel.kt | 76 +--- .../data/repository/DetailsRepositoryImpl.kt | 128 +----- .../repository/TranslationRepositoryImpl.kt | 66 +--- .../data/system/InstallationManagerImpl.kt | 16 +- .../data/translation/DeeplTranslator.kt | 3 - .../data/translation/GoogleTranslator.kt | 16 +- .../data/translation/LibreTranslator.kt | 4 +- .../details/data/translation/Translator.kt | 16 - .../data/translation/YoudaoTranslator.kt | 27 +- .../details/data/utils/preprocessMarkdown.kt | 124 ++---- .../domain/model/ApkValidationResult.kt | 4 +- .../domain/model/FingerprintCheckResult.kt | 3 +- .../domain/model/SaveInstalledAppParams.kt | 7 +- .../domain/system/AttestationVerifier.kt | 13 +- .../domain/system/InstallationManager.kt | 22 +- .../details/domain/util/VersionHelper.kt | 38 -- .../details/presentation/DetailsAction.kt | 46 --- .../details/presentation/DetailsRoot.kt | 24 +- .../details/presentation/DetailsState.kt | 102 +---- .../details/presentation/DetailsViewModel.kt | 366 ++---------------- .../components/ApkInspectSheet.kt | 15 +- .../presentation/components/AppHeader.kt | 6 +- .../components/InspectApkButton.kt | 13 +- .../presentation/components/LanguagePicker.kt | 1 - .../components/ReleaseAssetsPicker.kt | 27 +- .../components/SmartInstallButton.kt | 30 +- .../presentation/components/VersionPicker.kt | 7 +- .../presentation/components/sections/About.kt | 39 +- .../components/sections/Header.kt | 31 +- .../presentation/components/sections/Logs.kt | 5 - .../components/sections/ReleaseChannel.kt | 25 +- .../components/sections/WhatsNew.kt | 1 - .../presentation/markdown/AlertBlockQuote.kt | 2 +- .../markdown/ExpandableDetails.kt | 7 +- .../markdown/GithubStoreMarkdownComponents.kt | 30 +- .../markdown/SyntaxHighlightedCode.kt | 12 +- .../utils/MarkdownImageTransformer.kt | 117 +----- .../presentation/utils/MarkdownTruncate.kt | 29 +- .../DeveloperProfileRepositoryImpl.kt | 5 +- .../components/FavouriteRepositoryItem.kt | 6 +- .../impl/CachedRepositoriesDataSourceImpl.kt | 12 - .../data/repository/HomeRepositoryImpl.kt | 7 +- .../home/domain/repository/HomeRepository.kt | 6 - .../components/HomeTimeHelpers.kt | 12 - .../presentation/components/HotCardItem.kt | 2 +- .../profile/presentation/ProfileViewModel.kt | 18 +- .../data/repository/SearchRepositoryImpl.kt | 26 +- .../kotlin/zed/rainxch/domain/model/SortBy.kt | 7 - .../rainxch/search/presentation/SearchRoot.kt | 34 +- .../search/presentation/SearchViewModel.kt | 6 +- .../presentation/StarredReposViewModel.kt | 5 +- .../tweaks/presentation/RestartApp.android.kt | 10 +- .../rainxch/tweaks/presentation/RestartApp.kt | 16 - .../tweaks/presentation/TweaksAction.kt | 10 - .../tweaks/presentation/TweaksEvent.kt | 8 - .../rainxch/tweaks/presentation/TweaksRoot.kt | 3 +- .../tweaks/presentation/TweaksState.kt | 32 +- .../tweaks/presentation/TweaksViewModel.kt | 106 +---- .../components/CustomForgesDialog.kt | 4 +- .../components/sections/Installation.kt | 6 - .../components/sections/Language.kt | 19 +- .../components/sections/Network.kt | 27 +- .../components/sections/Translation.kt | 13 +- .../presentation/feedback/FeedbackEvent.kt | 4 +- .../feedback/FeedbackViewModel.kt | 7 +- .../feedback/components/ConditionalFields.kt | 2 +- .../components/FeedbackBottomSheet.kt | 5 - .../feedback/util/FeedbackComposer.kt | 2 +- .../hidden/HiddenRepositoriesViewModel.kt | 3 +- .../hosttokens/HostTokensAction.kt | 3 - .../hosttokens/HostTokensEvent.kt | 2 - .../presentation/hosttokens/HostTokensRoot.kt | 6 +- .../hosttokens/HostTokensState.kt | 7 +- .../hosttokens/HostTokensViewModel.kt | 14 +- .../mirror/MirrorPickerViewModel.kt | 2 +- .../presentation/model/ProxyScopeFormState.kt | 16 +- .../skipped/SkippedUpdatesViewModel.kt | 3 +- .../tweaks/presentation/RestartApp.jvm.kt | 21 +- 295 files changed, 658 insertions(+), 6688 deletions(-) delete mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/HostTokenInterceptor.kt diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt index 4d9488722..eb4ee1083 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt @@ -45,15 +45,8 @@ class MainActivity : ComponentActivity() { installSplashScreen() enableEdgeToEdge() - // Register activity result launcher for file picker (must be before STARTED) (shareManager as? AndroidShareManager)?.registerActivityResultLauncher(this) - // Apply the persisted language override BEFORE Compose kicks off - // so the very first frame resolves strings against the user's - // choice. `runBlocking` is acceptable here — DataStore reads are - // cheap and we only block once per Activity creation (including - // the post-language-swap recreate() path below). Without this, - // recreate() would briefly flash the old locale before settling. runBlocking { val tag = try { @@ -70,15 +63,6 @@ class MainActivity : ComponentActivity() { handleIncomingIntent(intent) - // Watch for runtime language changes from the Tweaks picker. - // Drop the initial emission (already applied above) and - // recreate() on any subsequent change — Android preserves - // `rememberSaveable` / ViewModel state through recreate, so - // scroll offsets, nav stack, and form fields all survive while - // every string re-resolves against the new locale. `key()` in - // the composition can't pull off the same trick: it changes - // the composite-key hash under it, which breaks - // `rememberSaveable` lookups and snaps LazyColumns back to 0. lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { tweaksRepository diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt index 25ba9243f..44bc460c0 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt @@ -32,24 +32,10 @@ class GithubStoreApp : Application() { override fun onCreate() { super.onCreate() - // Synchronous: Koin must be ready before the first composition can - // resolve any singleton; everything else can run on the app scope. initKoin { androidContext(this@GithubStoreApp) } - // Deferred to the app scope so they don't block the first frame. - // Each step gets its own launch + runCatching so a failure in one - // (NotificationManager hiccup, broadcast-registration race, etc.) - // doesn't abort the others — matches the pattern used by the - // other startup blocks below. - // - notification channels are only consumed when a worker posts - // a notification (minutes later at the earliest) - // - dynamic PackageEventReceiver is the in-process fast path; the - // manifest-registered receiver still catches broadcasts during - // the millisecond gap until the dynamic registration lands - // - the download notification observer has nothing to observe - // until the user starts a download appScope.launch { runCatching { createNotificationChannels() } .onFailure { Logger.w(it) { "Notification-channel creation failed" } } @@ -179,20 +165,9 @@ class GithubStoreApp : Application() { val existing = repo.getAppByPackage(selfPackageName) if (existing != null) { - // After a self-update the old process is killed before - // ACTION_PACKAGE_REPLACED can be delivered to our own - // receiver, so isPendingInstall stays true. Resolve it - // here at the earliest startup opportunity. if (existing.isPendingInstall) { resolveSelfPendingInstall(existing, repo) } else { - // Backfill for #515: pre-fix releases left rows - // with `installedVersion` (tag) pinned to the - // pre-update tag even though the system already - // holds the new APK. Detect the drift on cold - // start and normalize so checkForUpdates stops - // re-flagging the row as updatable on every - // periodic sweep. normalizeSelfInstalledVersion(existing, repo) } return@launch @@ -250,27 +225,6 @@ class GithubStoreApp : Application() { } } - /** - * Resolves a stale `isPendingInstall` flag for the app's own - * database row. Called on every cold start when the row exists - * and still has the flag set — the typical scenario after a - * successful self-update where the broadcast path missed. - */ - - /** - * Self-heal stale `installedVersion` tags on the self-row carried - * over from before #515 was fixed. Only fires when: - * - The row exists and is not pending an install. - * - The system's package versionCode matches the row's - * `installedVersionCode` (so the user's *system* says the - * install is finished). - * - The row's tag (`installedVersion`) doesn't match - * `latestVersion` (which the pre-install update worker wrote - * to the intended new tag). - * Sets `installedVersion = latestVersion` and clears - * `isUpdateAvailable`. Cheap, idempotent, side-effect-free - * otherwise. - */ private suspend fun normalizeSelfInstalledVersion( existing: InstalledApp, repo: InstalledAppsRepository, @@ -302,11 +256,7 @@ class GithubStoreApp : Application() { val systemInfo = packageMonitor.getInstalledPackageInfo(packageName) if (systemInfo != null) { val latestVersionCode = existing.latestVersionCode ?: 0L - // Also pin `installedVersion` (the tag string) to the - // intended new release. Otherwise `checkForUpdates` - // compares the freshly-fetched matched-release tag to - // the *previous* tag still in the row and re-flags - // isUpdateAvailable on every periodic sweep (#515). + val resolvedTag = existing.latestVersion ?: systemInfo.versionName repo.updateApp( existing.copy( diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt index 3c41c5ac2..a773a8ead 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.android.kt @@ -30,9 +30,6 @@ actual class OnboardingPermissionsController internal constructor( actual val installSourcesGranted: State = installSources actual fun requestNotifications() { - // POST_NOTIFICATIONS only requestable runtime on Android 13+. On older - // releases the permission was install-time, so callers should already - // see `granted = true` and never hit this branch. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { launchNotifications() } else { @@ -41,9 +38,6 @@ actual class OnboardingPermissionsController internal constructor( } actual fun requestInstallSources() { - // REQUEST_INSTALL_PACKAGES is a Settings toggle, not a runtime prompt. - // Deep-link to the per-app screen; the on-resume observer below re-reads - // `canRequestPackageInstalls()` once the user returns. val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { data = Uri.parse("package:${context.packageName}") @@ -83,8 +77,6 @@ actual fun rememberOnboardingPermissionsController(): OnboardingPermissionsContr ) } - // Re-read on resume so the install-sources Settings toggle reflects back - // when the user returns from the OS Settings screen. val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { val observer = @@ -106,8 +98,4 @@ private fun readNotificationsGranted(context: android.content.Context): Boolean ) == PackageManager.PERMISSION_GRANTED } -private fun readInstallSourcesGranted(context: android.content.Context): Boolean { - // canRequestPackageInstalls() exists since API 26 (Oreo). GHS minSdk is 26 - // (per top CLAUDE.md), so we don't need to gate on Build.VERSION_CODES.O. - return context.packageManager.canRequestPackageInstalls() -} +private fun readInstallSourcesGranted(context: android.content.Context): Boolean = context.packageManager.canRequestPackageInstalls() diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt index 242d333ed..f860905c3 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt @@ -103,7 +103,6 @@ fun App(deepLinkUri: String? = null) { } DeepLinkDestination.None -> { - // ignore unrecognized deep links } } } @@ -134,12 +133,6 @@ fun App(deepLinkUri: String? = null) { ) { ApplyAndroidSystemBars(state.isDarkTheme) - // Suppress the rate-limit dialog while the user is on the auth - // screen. They already accepted the prompt and are mid-sign-in; - // re-emitting the same dialog over the auth UI is noise that - // also blocks them from finishing the device-flow steps. Also - // flush any pending flag set by background API calls during - // auth, so it doesn't ghost back when the user returns home. val onAuthScreen = currentScreen is GithubStoreGraph.AuthenticationScreen LaunchedEffect(onAuthScreen, state.showRateLimitDialog) { if (onAuthScreen && state.showRateLimitDialog) { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt index 8554f9182..57f3ef939 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt @@ -6,11 +6,6 @@ sealed interface DeepLinkDestination { val repo: String, ) : DeepLinkDestination - /** - * Deep link to the apps tab. Used by the pending-install - * notification to bring the user back to the row where they can - * complete a deferred install. - */ data object Apps : DeepLinkDestination data class AuthHandoff( @@ -71,10 +66,6 @@ object DeepLinkParser { fun parse(uri: String): DeepLinkDestination { return when { - // Pending-install notification opens the apps tab. No path - // segments — the deferred install is keyed by package name - // on the row itself, so the link only needs to bring the - // user to the right tab. uri == "githubstore://apps" || uri == "githubstore://apps/" || uri.startsWith("githubstore://apps?") -> { DeepLinkDestination.Apps } @@ -121,10 +112,6 @@ object DeepLinkParser { } } - /** - * URL-decode a string, handling percent-encoded characters. - * Returns the original string if decoding fails. - */ private fun urlDecode(value: String): String = try { val result = StringBuilder() @@ -176,17 +163,6 @@ object DeepLinkParser { } } - /** - * Strictly validate owner and repo names to prevent injection attacks. - * Rejects: - * - Empty strings - * - Special characters that could be used for injection - * - Path traversal patterns - * - Control characters and whitespace - * - Excluded GitHub paths (like 'about', 'settings', etc.) - * - Names that exceed GitHub's length limits - * - Names that don't start with alphanumeric characters - */ private fun isStrictlyValidOwnerRepo( owner: String, repo: String, diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index 49440b803..a63a4c9fa 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -32,19 +32,12 @@ val viewModelsModule = viewModelOf(::ExternalImportViewModel) viewModelOf(::AuthenticationViewModel) viewModel { params -> - // Indexed access because `ownerParam` and `repoParam` are both - // Strings — positional `params.get()` would silently pick the - // first matching by type and could swap the two if Koin ever - // changes its resolution order. + DetailsViewModel( repositoryId = params.get(0), ownerParam = params.get(1), repoParam = params.get(2), isComingFromUpdate = params.get(3), - // Indexed access — `getOrNull()` would pick the - // first matching String (`ownerParam`) since type-based - // resolution doesn't disambiguate against the other - // String slots in this factory. sourceHostParam = if (params.size() > 4) params.get(4) else null, detailsRepository = get(), downloader = get(), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt index a12ffeb75..7c9d6fd26 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt @@ -42,15 +42,6 @@ import zed.rainxch.core.presentation.theme.fraunces import zed.rainxch.core.presentation.vocabulary.CookieShape import zed.rainxch.core.presentation.vocabulary.VersionStack -/** - * Cookie-active bottom nav (DESIGN.md §9.1). Active tab fills a [CookieShape] with - * `primary`, knocks the glyph out in `onPrimary`, and renders the label in Fraunces - * italic. Library tab shows a [VersionStack] badge when updates are pending. - * - * Heavy press-scale + spring physics matches D10 "rich motion." Per - * android-compose-ui skill: animated values drive `graphicsLayer` / `scale` to - * avoid recomposition. - */ @Composable fun BottomNavigation( currentScreen: GithubStoreGraph, @@ -132,7 +123,6 @@ private fun CookieTabItem( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(2.dp), ) { - // Cookie + glyph stack Box( modifier = Modifier.size(32.dp), contentAlignment = Alignment.Center, @@ -154,7 +144,7 @@ private fun CookieTabItem( tint = if (isSelected) activeFg else inactiveFg, ) } - // Active label — Fraunces italic + AnimatedVisibility( visible = isSelected, enter = fadeIn() + scaleIn(initialScale = 0.6f), @@ -175,7 +165,6 @@ private fun CookieTabItem( } } - // Update badge (Library tab) — VersionStack replaces M3 numeric badge if (showUpdateBadge) { Box( modifier = diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt index d3614a912..bb7f67574 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt @@ -43,6 +43,5 @@ object BottomNavigationUtils { ), ) - /** Bottom-nav (Android) shows the same items as the Desktop drawer. */ fun allowedScreens(): List = items() } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt index 6f372e5dc..6fc4d73ea 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt @@ -32,14 +32,6 @@ import zed.rainxch.core.presentation.vocabulary.VersionStack import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.app_name -/** - * Desktop sidebar drawer (DESIGN.md §8.1) — 240dp wide, persistent navigation. - * Cookie brand mark at top, nav items in middle, user card at bottom. Active item: - * `tintP` background + `primary` foreground. - * - * Wired by [AppNavigation] when the platform is non-Android. The Android path - * keeps the [BottomNavigation] capsule. - */ @Composable fun DesktopDrawer( currentScreen: GithubStoreGraph?, @@ -49,9 +41,7 @@ fun DesktopDrawer( modifier: Modifier = Modifier, ) { val cs = MaterialTheme.colorScheme - // Library (AppsScreen) is Android-only: Installer + PackageMonitor + Shizuku - // don't exist on Desktop, so the screen has nothing to manage. Filter it out - // of the drawer entirely rather than show an empty stub. + val items = BottomNavigationUtils.items().filterNot { it.screen == GithubStoreGraph.AppsScreen } Column( modifier = @@ -62,7 +52,6 @@ fun DesktopDrawer( .padding(vertical = 16.dp), verticalArrangement = Arrangement.Top, ) { - // Brand: Cookie + name Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt index 8556a2f2c..06765d925 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingPermissions.kt @@ -3,20 +3,6 @@ package zed.rainxch.githubstore.app.onboarding import androidx.compose.runtime.Composable import androidx.compose.runtime.State -/** - * Two-permission controller used by the Onboarding Permissions step (Android only). - * Wraps the platform's permission APIs so the composable layer stays expect-free. - * - * - [notificationsGranted]: `POST_NOTIFICATIONS` (Android 13+). On older API levels - * resolves to `true` immediately — the permission was install-time before T. - * - [installSourcesGranted]: `REQUEST_INSTALL_PACKAGES` settings toggle. There is - * no runtime prompt on Android — the user has to flip a Settings switch; we - * deep-link them there via [requestInstallSources] and re-read on resume. - * - * Desktop's actual is a no-op stub: both states report `true`, request methods do - * nothing. The Permissions step is skipped on Desktop anyway (D17) but the type - * exists in common so the screen composable can be platform-agnostic. - */ expect class OnboardingPermissionsController { val notificationsGranted: State val installSourcesGranted: State diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt index bca458d31..8546b7bcd 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingState.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ThemeMode -/** Onboarding step enum. Android shows all three; Desktop skips Permissions. */ enum class OnboardingStep { PALETTE, SIGN_IN, PERMISSIONS } @Stable diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt index 43c8543c8..4129314b4 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt @@ -35,14 +35,7 @@ class WhatsNewViewModel( private var lastLanguageTag: String? = null init { - // Re-load whenever the user's selected app language changes. - // The tag is threaded explicitly into the loader so we beat - // the race against MainActivity's setActiveLanguageTag — both - // collectors fan out from the same flow with no ordering - // guarantee, so reading Locale.getDefault() inside the loader - // can return the previous language. distinctUntilChanged - // guards against the initial replay-emit firing the load - // twice. + viewModelScope.launch { try { tweaksRepository diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt index bb51f2d73..147b0eaab 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt @@ -5,53 +5,16 @@ import java.awt.EventQueue import java.awt.Toolkit import java.util.concurrent.atomic.AtomicBoolean -/** - * Workaround for a Compose Multiplatform 1.10.x NPE on macOS where the native - * AX bridge (`sun.lwawt.macosx.CAccessible$AXChangeNotifier`) queries a - * Compose semantic node that has already been removed by Compose's own - * accessibility sync loop. The stack trace fingerprint is: - * - * androidx.compose.ui.platform.a11y.SemanticsOwnerAccessibility.accessibleParentOf - * -> sun.lwawt.macosx.CAccessible$AXChangeNotifier.propertyChange - * - * The NPE surfaces via two propagation paths and both are guarded here: - * - * 1. **EDT path** — NPE escapes `DispatchedTask.run` and propagates through - * the AWT event queue dispatch chain. [FilteringEventQueue] swallows it so - * the EDT keeps draining events. - * - * 2. **Coroutine-failure path** — `BaseContinuationImpl.resumeWith` catches the - * NPE and routes the coroutine failure through `handleCoroutineException` to - * the default uncaught-exception handler, bypassing [FilteringEventQueue]. - * The handler wrapper installed in [install] intercepts this path before it - * reaches [CrashReporter], preventing a spurious crash dump. - * - * Trade-off: macOS VoiceOver may miss updates on those removed nodes for the - * remainder of the session. Remove once the upstream fix lands (track against - * Compose MP 1.11+). - * - * See [GitHub-Store#330](https://github.com/OpenHub-Store/GitHub-Store/issues/330) - * and [GitHub-Store#640](https://github.com/OpenHub-Store/GitHub-Store/issues/640). - */ object A11yCrashGuard { - // Separate flags per path so each path logs its first suppression independently. private val warnedEdt = AtomicBoolean(false) private val warnedUncaught = AtomicBoolean(false) - // Must be called after CrashReporter.install() so the uncaught-exception handler - // chain is: A11yCrashGuard (filter) -> CrashReporter (log + dump) -> JVM default. fun install() { val osName = System.getProperty("os.name")?.lowercase().orEmpty() if (!osName.contains("mac")) return - // Path 1: NPE propagates out of the coroutine dispatcher and through the - // AWT EventQueue dispatch chain. Toolkit.getDefaultToolkit().systemEventQueue.push(FilteringEventQueue()) - // Path 2: NPE is intercepted by BaseContinuationImpl.resumeWith and - // forwarded to the default uncaught-exception handler via coroutine - // failure handling. Wrap the handler that CrashReporter already installed - // so all non-a11y exceptions still reach it. val previous = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> if (isComposeA11yNpe(throwable)) { @@ -63,8 +26,7 @@ object A11yCrashGuard { } return@setDefaultUncaughtExceptionHandler } - // Forward to CrashReporter (or JVM default if previous is null, which - // would only happen if install() is called before CrashReporter.install()). + previous?.uncaughtException(thread, throwable) ?: throwable.printStackTrace(System.err) } @@ -76,7 +38,7 @@ object A11yCrashGuard { while (current != null) { if (current.stackTrace.any { frame -> frame.className.startsWith("androidx.compose.ui.platform.a11y") || - // Specific AX bridge inner class present in all known traces for this bug. + frame.className.startsWith("sun.lwawt.macosx.CAccessible\$AXChangeNotifier") } ) { diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt index 1b66103a1..d1adaa868 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt @@ -32,40 +32,17 @@ import kotlin.system.exitProcess private const val LANGUAGE_PREF_READ_TIMEOUT_MS = 2000L fun main(args: Array) { - // Install first so anything that blows up during Koin init or - // resource loading leaves a diagnosable trail on disk (see - // `CrashReporter.resolveLogDir` for the per-OS path). CrashReporter.install() - // Guard the AWT EventDispatchThread against a known Compose MP 1.10.x NPE - // raised by the macOS accessibility bridge (see `A11yCrashGuard`). A11yCrashGuard.install() - // Skiko default backend on Linux is OpenGL, but on hybrid-GPU - // setups (e.g. AMD iGPU + Nvidia dGPU on Bazzite) the proprietary - // Nvidia driver path can SEGV the JVM before any Java exception - // handler runs (see issue #546). Honour an explicit - // `SKIKO_RENDER_API` env var if the user set one (escape hatch - // for reporters who can rescue the install with `SOFTWARE`), and - // otherwise leave Skiko to its default — pinning a specific API - // unconditionally would regress users who DO have a working - // accelerated path. selectLinuxRenderBackendIfRequested() - // Reduce JVM DNS cache TTL so network changes (VPN on/off) are picked up quickly. - // Default JVM caches positive lookups for 30s and negative lookups forever, - // which breaks connectivity when a VPN changes DNS/routing mid-session. java.security.Security.setProperty("networkaddress.cache.ttl", "30") java.security.Security.setProperty("networkaddress.cache.negative.ttl", "5") initKoin() - // Apply persisted UI language before any Compose code runs — same - // reasoning as on Android (see `MainActivity.onCreate`). Desktop - // Compose has no runtime `recreate()` equivalent, so mid-session - // language swaps surface as a "restart required" snackbar from the - // Tweaks screen; this block just covers the cold-start path so - // users see their chosen language immediately on next launch. runBlocking { val koin = GlobalContext.get() val tweaksRepo = koin.get() @@ -130,18 +107,6 @@ fun main(args: Array) { } } -/** - * Honour `SKIKO_RENDER_API` (env) or `-Dskiko.renderApi` (system - * property) on Linux so users hitting the Nvidia hybrid-GPU SEGV - * (issue #546) can rescue the install via: - * - * SKIKO_RENDER_API=SOFTWARE ./GitHub-Store-x86_64.AppImage - * - * Skiko reads the system property internally; we copy the env var - * into the property when present so AppImage launchers don't need - * to construct `-D` flags. Other platforms are skipped — Mac/Win - * are unaffected by this class of bug. - */ private fun selectLinuxRenderBackendIfRequested() { val osName = System.getProperty("os.name", "").lowercase() if (!osName.contains("linux")) return diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopDeepLink.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopDeepLink.kt index 9599f6868..78aa94575 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopDeepLink.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopDeepLink.kt @@ -13,12 +13,6 @@ object DesktopDeepLink { private const val SCHEME = "githubstore" private const val DESKTOP_FILE_NAME = "github-store-deeplink" - /** - * On Windows and Linux, ensure the `githubstore://` protocol is registered. - * - Windows: Writes to HKCU registry. - * - Linux: Creates a `.desktop` file and registers via `xdg-mime`. - * No-op on macOS (handled via Info.plist in the packaged .app). - */ fun registerUriSchemeIfNeeded() { when { isWindows() -> registerWindows() @@ -102,11 +96,6 @@ object DesktopDeepLink { runCommand("xdg-mime", "default", "$DESKTOP_FILE_NAME.desktop", "x-scheme-handler/$SCHEME") } - /** - * Try to forward a deep link URI to an already-running instance. - * @return `true` if the URI was forwarded (this instance should exit), - * `false` if no existing instance is running. - */ fun tryForwardToRunningInstance(uri: String): Boolean = try { Socket("127.0.0.1", SINGLE_INSTANCE_PORT).use { socket -> @@ -117,10 +106,6 @@ object DesktopDeepLink { false } - /** - * Start listening for URIs forwarded from new instances. - * Calls [onUri] on the main thread when a URI is received. - */ fun startInstanceListener(onUri: (String) -> Unit) { val thread = Thread({ diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt index ddee6fd77..d8a547376 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt @@ -53,7 +53,6 @@ import zed.rainxch.core.domain.utils.ShareManager actual val corePlatformModule = module { - // Core single { AndroidDownloader( @@ -61,7 +60,6 @@ actual val corePlatformModule = ) } - // AndroidInstaller — registered by class so the wrapper can inject it single { AndroidInstaller( context = get(), @@ -69,30 +67,24 @@ actual val corePlatformModule = ) } - // ShizukuServiceManager — manages Shizuku lifecycle, permissions, service binding single { ShizukuServiceManager( context = androidContext(), ).also { it.initialize() } } - // DhizukuServiceManager — manages Dhizuku lifecycle, permissions, service binding single { DhizukuServiceManager( context = androidContext(), ).also { it.initialize() } } - // RootServiceManager — detects Magisk/KernelSU/APatch su binaries, probes - // for grant status, executes silent installs via `pm install` over `su`. single { RootServiceManager( scope = get(), ).also { it.initialize() } } - // Installer — SilentInstallerDispatcher routes through the user's selected - // silent backend (Shizuku, Dhizuku, Root) or falls back to the standard installer. single { SilentInstallerDispatcher( androidContext = androidContext(), @@ -107,7 +99,6 @@ actual val corePlatformModule = } } - // InstallerStatusProvider — exposes Shizuku, Dhizuku, and Root availability to UI single { AndroidInstallerStatusProvider( shizukuServiceManager = get(), @@ -169,8 +160,6 @@ actual val corePlatformModule = AndroidLocalizationManager() } - // Locals - single { initDatabase(androidContext()) } @@ -204,8 +193,6 @@ actual val corePlatformModule = ) } - // Utils - single { AndroidBrowserHelper(androidContext()) } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_10_11.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_10_11.kt index 17edc353b..593ae06be 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_10_11.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_10_11.kt @@ -3,18 +3,6 @@ package zed.rainxch.core.data.local.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -/** - * Adds per-app preferred-variant tracking to the installed_apps table: - * - preferredAssetVariant: stable identifier (e.g. "arm64-v8a") for the - * asset the user wants to install. Survives version bumps because it's - * derived from the part of the filename that doesn't change. - * - preferredVariantStale: flipped to true by checkForUpdates when the - * persisted variant cannot be matched in a fresh release; the UI then - * prompts the user to pick again. - * - * Both columns default to safe "no preference" values so existing rows - * keep their current auto-pick behaviour. - */ val MIGRATION_10_11 = object : Migration(10, 11) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_11_12.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_11_12.kt index cb7d55396..42b3b4894 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_11_12.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_11_12.kt @@ -3,23 +3,6 @@ package zed.rainxch.core.data.local.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -/** - * Adds the multi-layer variant fingerprint columns to `installed_apps`: - * - * - `preferredAssetTokens`: serialized token-set fingerprint (closed - * vocabulary of arch / flavor tokens, sorted, joined with `|`) - * - `assetGlobPattern`: glob-pattern fingerprint with version-shaped - * substrings replaced by `*` - * - `pickedAssetIndex`: zero-based index of the picked asset in the - * release's installable-asset list (same-position fallback) - * - `pickedAssetSiblingCount`: total installable assets in the picked - * release, pairs with `pickedAssetIndex` - * - * All four columns are nullable so existing rows keep their current - * single-layer behaviour: the resolver falls back through the layers - * in order, and an old row with only `preferredAssetVariant` set still - * works via the legacy substring-tail match. - */ val MIGRATION_11_12 = object : Migration(11, 12) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_12_13.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_12_13.kt index e3e15a463..fdd004d53 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_12_13.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_12_13.kt @@ -3,18 +3,6 @@ package zed.rainxch.core.data.local.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -/** - * Adds `pendingInstallFilePath` to `installed_apps`. - * - * Set by `DefaultDownloadOrchestrator` when an - * `InstallPolicy.InstallWhileForeground` download finishes after the - * foreground screen has been destroyed. The apps list shows a - * "Ready to install" row with a one-tap install action when this is - * non-null. Cleared on successful install. - * - * Nullable, no default — existing rows have `null` and behave the - * same as before. - */ val MIGRATION_12_13 = object : Migration(12, 13) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_13_14.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_13_14.kt index d6e77cde8..a175e94d4 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_13_14.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_13_14.kt @@ -3,23 +3,6 @@ package zed.rainxch.core.data.local.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -/** - * Adds version + asset name metadata for parked downloads to - * `installed_apps`: - * - * - `pendingInstallVersion`: release tag of the version represented - * by `pendingInstallFilePath`. Lets the Details screen detect - * "the parked file matches the currently-selected release" and - * skip re-downloading on install. - * - `pendingInstallAssetName`: original (unscoped) asset filename - * of the parked file. Pairs with `pendingInstallVersion` for the - * Details-screen "ready to install" match. - * - * Both nullable, no default — existing rows have `null` for both - * and continue working unchanged (the apps list still shows - * "Ready to install" based on `pendingInstallFilePath` alone; only - * the Details-screen short-circuit needs the version match). - */ val MIGRATION_13_14 = object : Migration(13, 14) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_9_10.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_9_10.kt index 6446d522a..8c87a71c6 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_9_10.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_9_10.kt @@ -3,14 +3,6 @@ package zed.rainxch.core.data.local.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -/** - * Adds per-app monorepo tracking fields to the installed_apps table: - * - assetFilterRegex: optional regex applied to asset (file) names - * - fallbackToOlderReleases: when true, the update checker walks backwards - * through past releases until it finds one whose assets match the filter - * - * Both columns default to nullable / false so existing rows are unaffected. - */ val MIGRATION_9_10 = object : Migration(9, 10) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt index 89c9e577a..110342263 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt @@ -20,10 +20,7 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { } is ProxyConfig.System -> { - // java.net.ProxySelector.getDefault() does not read Android's - // per-network HTTP proxy. Android publishes the active proxy - // through standard system properties instead, which we resolve - // explicitly here so traffic actually flows through it. + proxy = resolveAndroidSystemProxy() } @@ -81,9 +78,7 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient { } internal fun resolveAndroidSystemProxy(): Proxy { - // System properties are user/OS-supplied, so guard against malformed - // values: InetSocketAddress(String, Int) throws IllegalArgumentException - // for ports outside 0..65535. + val httpsHost = System.getProperty("https.proxyHost")?.takeIf { it.isNotBlank() } val httpsPort = System.getProperty("https.proxyPort")?.toIntOrNull()?.takeIf { it in 1..65535 } if (httpsHost != null && httpsPort != null) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidAggressiveOemDetector.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidAggressiveOemDetector.kt index 12abb1359..388c35e4f 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidAggressiveOemDetector.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidAggressiveOemDetector.kt @@ -24,12 +24,7 @@ class AndroidAggressiveOemDetector( } override fun openBatteryOptimizationSettings(): Boolean = - // The targeted intent (`ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`) - // bypasses Play Store policy on standalone APKs and lands the user - // directly on the system whitelist toggle — but it's gated by the - // `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission, which Play - // restricts. Fall back to the per-app battery-optimization screen - // if the targeted action throws on a packaged build. + runCatching { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { @@ -58,9 +53,7 @@ class AndroidAggressiveOemDetector( } private companion object { - // Substring matches against `Build.BRAND` / `Build.MANUFACTURER` — - // covers vendor sub-brands (BBK group: Oppo, OnePlus, Realme, - // vivo, iQOO; Xiaomi: Redmi, Poco; Huawei: Honor). + private val AGGRESSIVE_OEMS = listOf( "oppo", diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt index b67731cd7..533a3a5df 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt @@ -31,20 +31,12 @@ class AndroidApkInspector( Logger.w(TAG) { "inspectFile: PackageManager refused $filePath" } return@withContext null } - // PM doesn't auto-populate sourceDir for archive-loaded - // ApplicationInfo, so loadLabel/loadIcon return generics - // unless we patch them here. + info.applicationInfo?.apply { sourceDir = filePath publicSourceDir = filePath } - // Wide catch by design: PackageManager / Resources reads - // can throw a long tail of unchecked exceptions - // (DeadObjectException, SecurityException, NPE on exotic - // signing layouts, etc.) that are all ultimately the same - // outcome from the caller's perspective — "inspection - // failed". Letting them escape leaves the sheet stuck on - // its loading spinner because the VM never updates state. + try { buildInspection( info = info, @@ -67,10 +59,7 @@ class AndroidApkInspector( } catch (_: PackageManager.NameNotFoundException) { null } catch (t: Throwable) { - // Wider catch for SecurityException, DeadObjectException - // and other binder-side surprises so the coroutine - // never propagates a PM hiccup; the sheet renders the - // empty state instead of crashing. + Logger.w(TAG) { "inspectInstalled: PackageManager threw for $packageName: $t" } @@ -155,8 +144,7 @@ class AndroidApkInspector( granted: Boolean?, isInstalledPackage: Boolean, ): ApkPermission { - // PermissionInfo lookup is best-effort — system / OEM - // permissions sometimes vanish between OS versions. + val info = runCatching { pm.getPermissionInfo(name, 0) }.getOrNull() val protection = info?.let { resolveProtectionLevel(it) } ?: ProtectionLevel.UNKNOWN @@ -165,12 +153,7 @@ class AndroidApkInspector( ?: name.substringAfterLast('.').replace('_', ' ').lowercase() .replaceFirstChar { it.titlecase() } val description = info?.loadDescription(pm)?.toString()?.takeIf { it.isNotBlank() } - // Normal-protection permissions are auto-granted at install, - // so on an installed package treat them as granted=true even - // if the requestedPermissionsFlags array didn't surface the - // bit (some OEM ROMs omit it for non-dangerous entries). For - // file-based inspections there's no grant state yet — report - // `null` so the UI can render "to be granted on install". + val resolvedGranted = when { granted != null -> granted @@ -218,9 +201,6 @@ class AndroidApkInspector( private companion object { const val TAG = "AndroidApkInspector" - // Minimum flags to populate everything the inspector reports. - // GET_SIGNING_CERTIFICATES is what SigningFingerprint reads; - // the rest power the counts and labels. @Suppress("DEPRECATION") val FULL_FLAGS: Int = ( diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloadProgressNotifier.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloadProgressNotifier.kt index dfa6e6ed2..d8a1186bc 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloadProgressNotifier.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloadProgressNotifier.kt @@ -13,26 +13,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import zed.rainxch.core.domain.system.DownloadProgressNotifier -/** - * Android implementation of [DownloadProgressNotifier]. - * - * # Behaviour - * - * - Ongoing notification on channel `app_downloads` (low-importance — - * no heads-up, no sound; long downloads shouldn't be noisy). - * - `setOnlyAlertOnce(true)` so repeated tick updates don't buzz. - * - `setOngoing(true)` prevents swipe-dismiss while downloading; - * cleared explicitly on completion / cancellation / failure. - * - Indeterminate spinner when the server omitted `Content-Length`. - * - "Cancel" action broadcasts [DownloadCancelReceiver.ACTION_CANCEL] - * with the package name; the receiver resolves the orchestrator - * via Koin and calls `cancel(packageName)`. - * - * # Permission gating - * - * POST_NOTIFICATIONS on Android 13+. If denied, silently skip — the - * orchestrator's in-app UI still reflects progress. - */ class AndroidDownloadProgressNotifier( private val context: Context, ) : DownloadProgressNotifier { @@ -47,12 +27,6 @@ class AndroidDownloadProgressNotifier( ) { if (!hasNotificationPermission()) return - // Encode the package in the Intent's data URI so PendingIntent - // identity (driven by Intent.filterEquals, which considers `data` - // but not extras) is uniquely per-package. Relying on - // packageName.hashCode() as requestCode alone risks a collision - // that would have FLAG_UPDATE_CURRENT overwrite another - // download's cancel extras. val cancelIntent = Intent(context, DownloadCancelReceiver::class.java).apply { action = DownloadCancelReceiver.ACTION_CANCEL @@ -108,11 +82,6 @@ class AndroidDownloadProgressNotifier( ) == PackageManager.PERMISSION_GRANTED } - /** - * Stable id in a range disjoint from the pending-install notifier - * (2000..2FFFFFF) and worker ids (1001..1005). Hash collisions are - * acceptable — worst case, two downloads share a single row. - */ private fun notificationIdFor(packageName: String): Int = NOTIFICATION_ID_BASE + (packageName.hashCode() and 0x00FFFFFF) @@ -144,7 +113,6 @@ class AndroidDownloadProgressNotifier( const val DOWNLOADS_CHANNEL_ID = "app_downloads" const val CANCEL_LABEL = "Cancel" - // Disjoint from PendingInstall (2000..) and workers (1001..). const val NOTIFICATION_ID_BASE = 3000 } } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt index 314dc83f4..912bee680 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt @@ -49,9 +49,7 @@ class AndroidDownloader( } is ProxyConfig.System -> { - // ProxySelector.getDefault() does not honor Android's - // per-network HTTP proxy; resolve it explicitly so - // downloads also flow through the device proxy. + proxy(resolveAndroidSystemProxy()) } @@ -92,9 +90,7 @@ class AndroidDownloader( suggestedFileName: String?, bypassMirror: Boolean, ): Flow = - // bypassMirror is a no-op here: this downloader uses OkHttp directly, - // not Ktor, so it never traverses MirrorRewriteInterceptor. The caller - // (MultiSourceDownloader) already passes the resolved direct/mirror URL. + flow { val client = buildClient() @@ -117,11 +113,7 @@ class AndroidDownloader( val downloadId = UUID.randomUUID().toString() val destination = File(dir, safeName) - // Each attempt writes to its own temp file so MultiSourceDownloader's - // direct/mirror race cannot have two jobs trampling the same path - // (see issue: "File not ready after download" with custom mirror). - // Temp lives in the same dir so the final rename stays on one FS - // and ATOMIC_MOVE works. + val tempFile = File(dir, "$safeName.part-$downloadId") if (tempFile.exists()) tempFile.delete() @@ -173,12 +165,7 @@ class AndroidDownloader( emit(DownloadProgress(finalDownloaded, total, finalPercent)) } } catch (e: kotlin.coroutines.cancellation.CancellationException) { - // Cancellation is the normal MultiSourceDownloader race - // outcome (loser racer cancelled when winner emits first - // progress). Don't log it as an error — that floods - // Logcat with confusing red lines that look like real - // download failures. Clean up the temp file and rethrow - // so structured concurrency stays correct. + tempFile.delete() throw e } catch (e: Exception) { @@ -203,9 +190,7 @@ class AndroidDownloader( StandardCopyOption.ATOMIC_MOVE, ) } catch (_: AtomicMoveNotSupportedException) { - // Fallback for filesystems without atomic-move support — still - // safer than writing directly to target because the partial bytes - // were never visible at `target` until this step. + Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING) } } @@ -253,9 +238,7 @@ class AndroidDownloader( override suspend fun cancelDownload(fileName: String): Boolean = withContext(Dispatchers.IO) { - // Cancel every in-flight download for this fileName — MultiSource - // races run direct + mirror in parallel under the same logical - // name, so a single-id lookup would leave one of them running. + val ids = idsByName.remove(fileName)?.toList().orEmpty() if (ids.isEmpty()) return@withContext false @@ -267,10 +250,7 @@ class AndroidDownloader( cancelled = true } } - // No destination delete: the flow's catch handles its own temp - // file. The final destination is only written via atomic-rename - // on success, so it's either a prior valid download (keep) or - // doesn't exist yet. + cancelled } } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt index 0ba20306b..eb415f2cf 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidFileLocationsProvider.kt @@ -39,9 +39,7 @@ class AndroidFileLocationsProvider( } Log.w(TAG, "Downloads dir candidate unusable: ${dir.absolutePath}") } - // Last-resort fallback. context.filesDir always exists for an - // installed app; if even this fails the device is in an - // unrecoverable state and a thrown exception wouldn't help. + val fallback = File(context.filesDir, "downloads") fallback.mkdirs() return fallback.absolutePath @@ -65,7 +63,7 @@ class AndroidFileLocationsProvider( } override fun setExecutableIfNeeded(path: String) { - // No-op on Android + } override fun getCacheSizeBytes(): Long { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstallerInfoExtractor.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstallerInfoExtractor.kt index c7fac9f36..319cabfb4 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstallerInfoExtractor.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstallerInfoExtractor.kt @@ -59,11 +59,6 @@ class AndroidInstallerInfoExtractor( } } - /** - * Tries to parse the APK with full flags first (metadata + signing). - * If that fails, retries with minimal flags since some APKs / Android - * versions choke on GET_SIGNING_CERTIFICATES combined with other flags. - */ private fun parseApk( packageManager: PackageManager, filePath: String, @@ -80,8 +75,6 @@ class AndroidInstallerInfoExtractor( "Full-flag parse failed for $filePath, retrying with minimal flags" } - // Retry without signing — the fingerprint will be extracted - // separately in extractSigningFingerprint. val minimalFlags = PackageManager.GET_META_DATA val minimal = getPackageArchiveInfoCompat(packageManager, filePath, minimalFlags) if (minimal != null) return minimal @@ -108,27 +101,20 @@ class AndroidInstallerInfoExtractor( packageManager.getPackageArchiveInfo(filePath, flags) } - /** - * Extracts the signing fingerprint from an already-parsed PackageInfo - * if available, otherwise does a separate lightweight parse with only - * the signing flag. - */ private fun extractSigningFingerprint( packageManager: PackageManager, packageInfo: android.content.pm.PackageInfo, filePath: String, ): String? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // Try from the already-parsed info first (works when - // the full-flag parse succeeded). + val sigInfo = packageInfo.signingInfo if (sigInfo != null) { val cert = if (sigInfo.hasMultipleSigners()) { sigInfo.apkContentsSigners?.firstOrNull() } else { - // History is oldest→newest; last entry is the - // current signer after key rotation. + sigInfo.signingCertificateHistory?.lastOrNull() } cert?.toByteArray()?.let { certBytes -> @@ -136,8 +122,6 @@ class AndroidInstallerInfoExtractor( } } - // Signing info missing (minimal-flag fallback path) — - // do a separate parse with only the signing flag. val sigOnly = getPackageArchiveInfoCompat( packageManager, filePath, diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidLocalizationManager.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidLocalizationManager.kt index faf082422..5665088d8 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidLocalizationManager.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidLocalizationManager.kt @@ -3,11 +3,7 @@ package zed.rainxch.core.data.services import java.util.Locale class AndroidLocalizationManager : zed.rainxch.core.data.services.LocalizationManager { - /** - * Snapshot of the original JVM locale at construction time, so - * [setActiveLanguageTag] with a null argument can restore it even - * after prior overrides have modified `Locale.getDefault()`. - */ + private val systemDefault: Locale = Locale.getDefault() override fun getCurrentLanguageCode(): String { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidPendingInstallNotifier.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidPendingInstallNotifier.kt index 91559b5d2..1dcd6ec45 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidPendingInstallNotifier.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidPendingInstallNotifier.kt @@ -13,28 +13,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import zed.rainxch.core.domain.system.PendingInstallNotifier -/** - * Android implementation of [PendingInstallNotifier]. - * - * # Behaviour - * - * - Each pending install gets its own notification, keyed by the - * package name's hash code (so two pending installs of different - * apps don't replace each other in the shade). - * - Tapping the notification (or the explicit "Open" action) launches - * `MainActivity` via the `githubstore://apps` deep link, which the - * parser routes to the apps tab where the user finishes the install. - * - Channel `app_updates` (already declared in `GithubStoreApp`) is - * reused — pending installs are conceptually a flavour of update - * notification and don't deserve a separate channel. - * - * # Permission gating - * - * On Android 13+, posting requires `POST_NOTIFICATIONS`. We check it - * silently rather than throwing — if the user denied the permission, - * the install still completes, they just don't get notified. The apps - * row still shows the pending state, so this is a graceful degrade. - */ class AndroidPendingInstallNotifier( private val context: Context, ) : PendingInstallNotifier { @@ -48,21 +26,13 @@ class AndroidPendingInstallNotifier( ) { if (!hasNotificationPermission()) return - // Deep link straight to the *Details* page for this specific - // app. The existing `githubstore://repo/owner/name` route is - // already wired in `DeepLinkParser` and lands on Details with - // the right repository pre-loaded — the user can tap install - // immediately. The Details page also detects the parked file - // via `pendingInstallFilePath` and skips re-downloading. val safeOwner = sanitizeForUri(repoOwner) val safeRepo = sanitizeForUri(repoName) val uri = if (safeOwner.isNotEmpty() && safeRepo.isNotEmpty()) { "githubstore://repo/$safeOwner/$safeRepo" } else { - // Synthetic-key fallback (e.g. for fresh installs - // where we don't know the package's owner/repo - // yet) — open the apps tab as before. + FALLBACK_URI } val deepLinkIntent = @@ -109,24 +79,9 @@ class AndroidPendingInstallNotifier( ) == PackageManager.PERMISSION_GRANTED } - /** - * Stable per-package notification id. We hash the package name and - * mask off the high bits so it lands in a positive int range that - * doesn't collide with the existing notification ids used by the - * update workers (1004, 1005). Hash collisions are vanishingly - * rare in practice for package names; if they happen the worst - * case is that two pending installs share a notification, which - * is the same outcome as having only one notifier. - */ private fun notificationIdFor(packageName: String): Int = NOTIFICATION_ID_BASE + (packageName.hashCode() and 0x00FFFFFF) - /** - * Defensive sanitisation for the deep-link path components. - * `DeepLinkParser.isStrictlyValidOwnerRepo` already rejects - * weird input, so this just strips characters that would break - * URI parsing before they reach the parser. - */ private fun sanitizeForUri(input: String): String { if (input.isBlank()) return "" return input.filter { ch -> @@ -139,8 +94,6 @@ class AndroidPendingInstallNotifier( const val FALLBACK_URI = "githubstore://apps" const val SUBTEXT = "Ready to install" - // Existing worker notifications use 1001-1005; start the - // pending-install id space comfortably above that. const val NOTIFICATION_ID_BASE = 2000 } } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AutoUpdateWorker.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AutoUpdateWorker.kt index 0c105b521..7be6bbc21 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AutoUpdateWorker.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AutoUpdateWorker.kt @@ -32,14 +32,6 @@ import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.Installer import zed.rainxch.core.domain.system.SystemInstallSerializer -/** - * Background worker that automatically downloads and silently installs - * available updates via Shizuku. - * - * Only runs when auto-update is enabled AND Shizuku installer is selected and READY. - * Falls back gracefully: if Shizuku becomes unavailable mid-update, remaining apps - * are skipped and a notification is shown for manual update. - */ class AutoUpdateWorker( context: Context, params: WorkerParameters, @@ -165,7 +157,7 @@ class AutoUpdateWorker( } Logger.d { "AutoUpdateWorker: Downloading $assetName for ${app.appName}" } - downloader.download(assetUrl, assetName).collect { /* consume flow to completion */ } + downloader.download(assetUrl, assetName).collect { } val filePath = downloader.getDownloadedFilePath(assetName) @@ -174,7 +166,6 @@ class AutoUpdateWorker( val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath) - // Validate package name matches (only when extraction succeeded) if (apkInfo != null && apkInfo.packageName != app.packageName) { Logger.e { "AutoUpdateWorker: Package name mismatch for ${app.appName}! " + diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/BootReceiver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/BootReceiver.kt index e5e36537b..4d7572d71 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/BootReceiver.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/BootReceiver.kt @@ -9,10 +9,6 @@ import kotlinx.coroutines.runBlocking import org.koin.core.context.GlobalContext import zed.rainxch.core.domain.repository.TweaksRepository -/** - * Reschedules periodic update checks after device reboot. - * Registered statically in AndroidManifest.xml. - */ class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return @@ -35,11 +31,7 @@ class BootReceiver : BroadcastReceiver() { UpdateScheduler.cancel(context) } } catch (t: Throwable) { - // Don't let scheduling failures crash the framework's - // broadcast pipeline — propagation triggers a second - // `sendFinished` from the platform after our own - // `pendingResult.finish()` and surfaces as - // `IllegalStateException: Broadcast already finished`. + Logger.e(t) { "BootReceiver: scheduling failed; dropped" } } finally { pendingResult.finish() diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadCancelReceiver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadCancelReceiver.kt index d3f876cc7..ec71cdb4f 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadCancelReceiver.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadCancelReceiver.kt @@ -9,21 +9,6 @@ import kotlinx.coroutines.launch import org.koin.core.context.GlobalContext import zed.rainxch.core.domain.system.DownloadOrchestrator -/** - * Receives the "Cancel" action fired by the download progress - * notification (see [AndroidDownloadProgressNotifier]) and routes it to - * [DownloadOrchestrator.cancel]. - * - * Declared in the manifest so it fires even when the app is in the - * background and the process has been trimmed — static registration is - * what Android guarantees survival for post-notification callbacks. - * - * Koin's [GlobalContext] is used to resolve the orchestrator and the - * application-scoped [CoroutineScope] because a manifest-registered - * receiver has no injection point; the orchestrator is already a - * singleton so `GlobalContext.get()` returns the same instance the rest - * of the app uses. - */ class DownloadCancelReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action != ACTION_CANCEL) return @@ -33,9 +18,6 @@ class DownloadCancelReceiver : BroadcastReceiver() { return } - // goAsync keeps the BroadcastReceiver alive long enough for the - // suspend call to complete. Without it, the receiver returns as - // soon as onReceive exits and the coroutine may be killed. val pending = goAsync() val koin = GlobalContext.getOrNull() if (koin == null) { diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadNotificationObserver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadNotificationObserver.kt index 1c2d41017..16b213e94 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadNotificationObserver.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/DownloadNotificationObserver.kt @@ -10,48 +10,6 @@ import zed.rainxch.core.domain.system.DownloadProgressNotifier import zed.rainxch.core.domain.system.DownloadStage import zed.rainxch.core.domain.system.OrchestratedDownload -/** - * Single long-lived subscriber that translates - * [DownloadOrchestrator.downloads] state transitions into calls on - * [DownloadProgressNotifier]. - * - * # Why it's its own class - * - * The orchestrator stays platform-agnostic (common code, no Android - * imports) and doesn't know about notifications. The observer lives on - * `androidMain` alongside the Android notifier and is only started from - * [zed.rainxch.githubstore.app.GithubStoreApp], which means the whole - * feature is Android-only without any platform branches in shared code. - * - * # Transition rules - * - * - `Queued`, `Downloading` → post / update progress notification. - * - Anything else (`Installing`, `AwaitingInstall`, `Completed`, - * `Cancelled`, `Failed`) or entry removal → clear. `AwaitingInstall` - * is owned by [zed.rainxch.core.domain.system.PendingInstallNotifier] - * which posts its own "ready to install" row. - * - * # Throttling - * - * The orchestrator emits on every ~8KB chunk (hundreds of emissions - * per second on a fast link). Android silently drops notification - * updates posted faster than ~200ms for the same id, and every - * `NotificationManagerCompat.notify` is a Binder round-trip, so - * letting every emission through both wastes CPU and produces a stuck - * progress bar that jumps at the end. - * - * We coalesce in-stage ticks to at most one post per - * [PROGRESS_UPDATE_INTERVAL_MS] per package, but always flush - * immediately on stage transitions (`Queued → Downloading`, - * `Downloading → Completed`, etc.) and on 100%-percent emissions so - * the final frame is never skipped. - * - * # Lifecycle - * - * Started once from the Application's `onCreate` via [start], collected - * on the app-scoped coroutine scope (same one Koin provides). No - * explicit stop — the process going away ends the flow. - */ class DownloadNotificationObserver( private val orchestrator: DownloadOrchestrator, private val notifier: DownloadProgressNotifier, @@ -71,25 +29,19 @@ class DownloadNotificationObserver( try { reconcile(snapshot) } catch (t: Throwable) { - // Never let a NotificationManager hiccup - // collapse the whole flow — progress - // notifications are best-effort. + Logger.w(t) { "DownloadNotificationObserver: reconcile failed, continuing" } } } } finally { - // Reset so a subsequent start() on this process - // (e.g. after the caller's scope restarts) can - // resubscribe instead of silently no-op'ing on the - // `job?.isActive == true` guard. + job = null } } } private fun reconcile(snapshot: Map) { - // Clear notifications for entries that vanished from the map - // (e.g. dismissed after Completed, or cleared on Cancelled). + val removed = lastStages.keys - snapshot.keys for (pkg in removed) { clearProgressSafely(pkg) @@ -150,8 +102,7 @@ class DownloadNotificationObserver( } private companion object { - // Comfortably above Android's ~200ms internal drop threshold - // while still feeling live to the eye. + const val PROGRESS_UPDATE_INTERVAL_MS = 400L } } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt index 14faa9a22..54dee2742 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt @@ -20,15 +20,6 @@ import zed.rainxch.core.domain.system.SystemInstallSerializer import zed.rainxch.core.domain.util.VersionVerdict import zed.rainxch.core.domain.util.resolveExternalInstallVerdict -/** - * Listens for package install/replace/remove broadcasts to update tracked app state. - * - * Registered both statically (manifest — works when process is dead, e.g. after - * Shizuku silent install) and dynamically (GithubStoreApp — immediate in-process delivery). - * - * Uses [KoinComponent] for the no-arg constructor path (manifest-registered). - * The constructor with explicit dependencies is used for dynamic registration. - */ class PackageEventReceiver() : BroadcastReceiver(), KoinComponent { @@ -39,7 +30,6 @@ class PackageEventReceiver() : private val externalLinkDaoKoin: ExternalLinkDao by inject() private val systemInstallSerializerKoin: SystemInstallSerializer by inject() - // Explicitly provided dependencies (dynamic registration path) private var explicitRepository: InstalledAppsRepository? = null private var explicitMonitor: PackageMonitor? = null private var explicitExternalImport: ExternalImportRepository? = null @@ -47,11 +37,6 @@ class PackageEventReceiver() : private var explicitAppScope: CoroutineScope? = null private var explicitSystemInstallSerializer: SystemInstallSerializer? = null - // Local fallback scope for the manifest-registered path when - // `onReceive` fires but Koin somehow couldn't resolve the shared - // app scope (extremely unlikely — the Application installs Koin - // synchronously in onCreate). The async backstop below prefers - // the Koin scope via `getBackstopScope`. private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) constructor( @@ -84,18 +69,14 @@ class PackageEventReceiver() : explicitSystemInstallSerializer ?: systemInstallSerializerKoin private fun getBackstopScope(): CoroutineScope = - // Koin's app-scoped CoroutineScope outlives a manifest-registered - // receiver whose local `scope` would die with the instance. Fall - // back to the local scope only if Koin isn't initialized yet - // (shouldn't happen post-Application.onCreate, but defensive). + explicitAppScope ?: runCatching { appScopeKoin }.getOrElse { scope } override fun onReceive( context: Context?, intent: Intent?, ) { - // MY_PACKAGE_REPLACED has no data URI — the target is the - // receiving app itself. Fall back to the context's package name. + val packageName = intent?.data?.schemeSpecificPart ?: if (intent?.action == Intent.ACTION_MY_PACKAGE_REPLACED) { context?.packageName @@ -124,13 +105,6 @@ class PackageEventReceiver() : } } - /** - * Wipes the parked-install metadata on the row and deletes the - * APK file from disk now that the system holds the real package. - * Best-effort throughout — DB write failures are logged but never - * thrown so the broadcast handler can continue to do its other work - * (delta-scan, etc.). Safe to call when no parked file exists. - */ private suspend fun clearParkedInstall( repo: InstalledAppsRepository, packageName: String, @@ -150,9 +124,7 @@ class PackageEventReceiver() : } private suspend fun onPackageInstalled(packageName: String) { - // Release the system-install serializer gate so the next queued - // install (if any) can fire its ACTION_VIEW intent. Done at the - // very top so the gate clears even if downstream DB writes fail. + getSystemInstallSerializer().markCompleted(packageName) try { @@ -160,10 +132,6 @@ class PackageEventReceiver() : val monitor = getMonitor() val app = repo.getAppByPackage(packageName) - // First-time installs (app == null) skip the tracked-app branches - // but MUST still hit the backstop delta-scan launch below — that's - // how a freshly-installed GitHub app surfaces as a wizard candidate - // when the user installs it after the initial scan. if (app != null) { if (app.isPendingInstall) { val systemInfo = monitor.getInstalledPackageInfo(packageName) @@ -172,11 +140,7 @@ class PackageEventReceiver() : val versionCodeMatchesTarget = expectedVersionCode > 0L && systemInfo.versionCode >= expectedVersionCode - // When latestVersionCode is 0 (apkInfo extraction - // failed pre-download) the versionCode comparison - // can't decide. Fall back to versionName so the - // tag still gets written and the apps row stops - // rendering the stale "installed: vOld" subtext. + val versionNameChanged = !systemInfo.versionName.isNullOrBlank() && systemInfo.versionName != app.installedVersionName @@ -184,15 +148,6 @@ class PackageEventReceiver() : versionCodeMatchesTarget || (expectedVersionCode <= 0L && versionNameChanged) - // Source-of-truth for the tag the user just - // installed: `pendingInstallVersion` carries the - // exact release tag the orchestrator parked the - // file under (set during download flow). It's - // honest about explicit older-version installs; - // `app.latestVersion` is GHS's cached idea of - // "what GitHub says is latest" and would show - // the wrong tag if the user picked an older - // release from the version picker. val installedTag = app.pendingInstallVersion ?: app.latestVersion @@ -210,14 +165,7 @@ class PackageEventReceiver() : repo.updatePendingStatus(packageName, false) Logger.i { "Update confirmed via broadcast: $packageName (v${systemInfo.versionName}, tag=$installedTag)" } } else { - // Even on the "didn't reach target" branch the - // installedVersion tag must move forward when the - // user accepted some install — leaving it pinned - // to the old tag makes the apps row report the - // pre-install version forever. Use the parked - // tag so an explicit older-version install is - // honoured (issue: details + apps screens were - // showing latest when user picked older). + repo.updateApp( app.copy( isPendingInstall = false, @@ -241,10 +189,7 @@ class PackageEventReceiver() : repo.updatePendingStatus(packageName, false) Logger.i { "Resolved pending install via broadcast (no system info): $packageName" } } - // System has the package now — the parked APK on disk is - // dead weight. Clear the path on the row (otherwise the - // apps screen keeps rendering the "Install" CTA after a - // successful install) and delete the file to free space. + clearParkedInstall(repo, packageName, app.pendingInstallFilePath) } else { handleExternalInstall(packageName, app, repo, monitor) @@ -254,11 +199,6 @@ class PackageEventReceiver() : Logger.e { "PackageEventReceiver error for $packageName: ${e.message}" } } - // Fire a delta scan for previously-untracked installs so the - // import banner can pick up the new candidate. Guarded so we - // don't churn on apps the user already linked or asked us to - // ignore. Runs on the app scope — independent of the install - // path above. Always fires regardless of whether `app` was found. getBackstopScope().launch { runCatching { val rescan = shouldRescan(packageName) @@ -271,13 +211,6 @@ class PackageEventReceiver() : } } - // Skip re-scanning when (a) we already track the app in - // `installed_apps` (the user installed it through the store, or - // we already auto-linked it and materialized the row), or (b) the - // package is already MATCHED / NEVER_ASK in `external_links`. - // PENDING_REVIEW and SKIPPED are intentionally rescanned — - // metadata may have changed (label, fingerprint, installer) and - // the user hasn't given a permanent answer yet. private suspend fun shouldRescan(packageName: String): Boolean { val tracked = runCatching { getRepository().getAppByPackage(packageName) } .getOrNull() @@ -288,37 +221,6 @@ class PackageEventReceiver() : state != ExternalLinkState.NEVER_ASK.name } - /** - * Path taken when the broadcast fires for a tracked app that the - * user did NOT install from inside the store (sideload, browser - * download, Play Store update, F-Droid update of a shared - * package, etc.). The pending-install branch above handles the - * in-app install case. - * - * Strategy (GitHub-Store#378): - * - * 1. Refresh every version field from PackageManager — this is - * the strictest source of truth for what is actually on - * device right now. - * 2. Apply [resolveExternalInstallVerdict] for an immediate - * decision about `isUpdateAvailable`. The resolver uses a - * priority ladder (versionCode → versionName vs - * latestVersionName → versionName vs release tag) and only - * returns [VersionVerdict.UNKNOWN] when none of those - * produce a reliable answer. - * 3. Dispatch an async `checkForUpdates(packageName)` on the - * app-scoped coroutine scope. That call re-fetches the - * latest release list from GitHub and applies - * [zed.rainxch.core.domain.util.VersionMath] with the freshly - * updated `installedVersion`, so even an incorrect optimistic - * verdict is corrected within the RTT of a single GitHub - * API hit. - * - * The async backstop runs on the Koin-provided app scope so it - * survives the receiver instance being torn down after - * `onReceive` returns — critical for the manifest-registered - * path. - */ private suspend fun handleExternalInstall( packageName: String, app: zed.rainxch.core.domain.model.InstalledApp, @@ -347,17 +249,10 @@ class PackageEventReceiver() : when (verdict) { VersionVerdict.UP_TO_DATE -> false VersionVerdict.UPDATE_AVAILABLE -> true - // Preserve the current flag for UNKNOWN — the async - // checkForUpdates below is about to overwrite it with - // an authoritative answer anyway. + VersionVerdict.UNKNOWN -> app.isUpdateAvailable } - // Targeted column-only write: avoids clobbering sibling fields - // (download orchestrator metadata, variant pin, favourite - // toggle, checkForUpdates results…) that may have landed - // between `onPackageInstalled`'s initial `getAppByPackage` and - // this write. See `InstalledAppsRepository.updateInstalledVersion`. repo.updateInstalledVersion( packageName = packageName, installedVersion = systemInfo.versionName, @@ -373,8 +268,6 @@ class PackageEventReceiver() : "verdict=$verdict, updateAvailable=$newIsUpdateAvailable" } - // Authoritative re-validation against fresh GitHub release data. - // Runs on the app scope so it outlives this broadcast. getBackstopScope().launch { try { repo.checkForUpdates(packageName) @@ -390,8 +283,7 @@ class PackageEventReceiver() : } private suspend fun onPackageRemoved(packageName: String) { - // Mirror onPackageInstalled — release the install gate so a queued - // re-install for the same package isn't held up. + getSystemInstallSerializer().markCompleted(packageName) try { @@ -399,23 +291,13 @@ class PackageEventReceiver() : runCatching { getExternalImport().unlink(packageName) } .onFailure { initialError -> Logger.w(initialError) { "External link cleanup failed for $packageName; scheduling retry" } - // A failed unlink leaves a stale MATCHED/NEVER_ASK row that - // makes `shouldRescan` return false on a future reinstall — - // i.e., the user reinstalls a previously-tracked app and we - // silently fail to re-link it. Retry once on the app scope - // after a short backoff. If the retry also fails, the next - // periodic worker sweep gets a chance via `runPeriodicExternalDeltaScan`. + getBackstopScope().launch { kotlinx.coroutines.delay(UNLINK_RETRY_DELAY_MS) runCatching { getExternalImport().unlink(packageName) } .onSuccess { Logger.i { "External link cleanup retry succeeded for $packageName" } - // Recovery delta scan: a fast reinstall during the - // retry backoff window would have hit shouldRescan - // while the row was still MATCHED → no rescan - // queued. Now that the row is gone, evaluate - // shouldRescan again and fire if the package is - // currently installed. + runCatching { if (shouldRescan(packageName)) { getExternalImport().runDeltaScan(setOf(packageName)) diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/SigningFingerprint.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/SigningFingerprint.kt index 98115b166..b85b69e1e 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/SigningFingerprint.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/SigningFingerprint.kt @@ -4,15 +4,6 @@ import android.content.pm.PackageInfo import android.os.Build import java.security.MessageDigest -/** - * Pulls the SHA-256 signing fingerprint out of a [PackageInfo], - * regardless of whether it was parsed with `GET_SIGNING_CERTIFICATES` - * (Android P+) or the legacy `GET_SIGNATURES` flag. Returns `null` - * when no signature data is reachable. - * - * Format: hex bytes joined with `:` separators, uppercase — same shape - * as `keytool -printcert` so users can paste-compare. - */ internal object SigningFingerprint { fun fromPackageInfo(info: PackageInfo): String? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -21,8 +12,7 @@ internal object SigningFingerprint { if (sigInfo.hasMultipleSigners()) { sigInfo.apkContentsSigners?.firstOrNull() } else { - // signingCertificateHistory is oldest → newest; the - // last entry is the active signer after rotation. + sigInfo.signingCertificateHistory?.lastOrNull() } return cert?.toByteArray()?.let(::sha256) diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateCheckWorker.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateCheckWorker.kt index c687a2569..32283c303 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateCheckWorker.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateCheckWorker.kt @@ -26,15 +26,6 @@ import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.PackageMonitor import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase -/** - * Periodic background worker that checks all tracked installed apps for available updates. - * - * Runs via WorkManager on a configurable schedule (default: every 6 hours). - * First syncs app state with the system package manager, then checks each - * tracked app's GitHub repository for new releases. - * Shows a notification when updates are found, or triggers auto-update - * if Shizuku silent install is enabled and auto-update preference is on. - */ class UpdateCheckWorker( context: Context, params: WorkerParameters, @@ -51,22 +42,19 @@ class UpdateCheckWorker( try { Logger.i { "UpdateCheckWorker: Starting periodic update check" } - // Run as foreground service to prevent OS from killing the worker setForeground(createForegroundInfo("Checking for updates...")) - // First sync installed apps state with system val syncResult = syncInstalledAppsUseCase() if (syncResult.isFailure) { Logger.w { "UpdateCheckWorker: Sync had issues: ${syncResult.exceptionOrNull()?.message}" } } - // Check all tracked apps for updates installedAppsRepository.checkAllForUpdates() val appsWithUpdates = installedAppsRepository.getAppsWithUpdates().first() if (appsWithUpdates.isNotEmpty()) { - // Check if auto-update via Shizuku is enabled + val autoUpdateEnabled = tweaksRepository.getAutoUpdateEnabled().first() val installerType = tweaksRepository.getInstallerType().first() @@ -77,7 +65,7 @@ class UpdateCheckWorker( } UpdateScheduler.scheduleAutoUpdate(applicationContext) } else { - // Show notification for manual update + showUpdateNotification(appsWithUpdates) } } else { @@ -97,9 +85,6 @@ class UpdateCheckWorker( } } - // Periodic best-effort: catch packages whose ACTION_PACKAGE_ADDED - // broadcast we missed (process killed, OEM app-standby, etc.). - // Cap at 50 so a 200-package device doesn't drag the worker. private suspend fun runPeriodicExternalDeltaScan() { try { val installed = packageMonitor.getAllInstalledPackageNames() @@ -151,9 +136,9 @@ class UpdateCheckWorker( } } - @SuppressLint("MissingPermission") // Permission checked at runtime before notify() + @SuppressLint("MissingPermission") private suspend fun showUpdateNotification(appsWithUpdates: List) { - // Check notification permission for API 33+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val granted = ContextCompat.checkSelfPermission( diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt index a80f4c8fc..b23f8c8ed 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt @@ -46,12 +46,6 @@ object UpdateScheduler { request = request, ) - // Note: an expedited request CANNOT carry an initial delay — - // WorkManager throws `IllegalArgumentException: Expedited jobs - // cannot be delayed` at build time. Drop the cold-start backoff - // and let the OS scheduler dispatch as soon as the network - // constraint is satisfied; on aggressive-OEM ROMs the expedited - // tier is what actually gets the work to run. val immediateRequest = OneTimeWorkRequestBuilder() .setConstraints(constraints) @@ -116,13 +110,7 @@ object UpdateScheduler { 15, TimeUnit.MINUTES, ) - // Intentionally NOT expedited: AutoUpdateWorker downloads - // multiple APKs and installs them sequentially, which can - // easily exceed the 10-minute expedited time limit and - // get the worker terminated mid-install. The worker - // already promotes itself to a foreground service via - // setForeground, which gives it the headroom it needs - // without the expedited time cap. + .build() WorkManager diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuInstallerServiceImpl.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuInstallerServiceImpl.kt index 934786060..eb58cac62 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuInstallerServiceImpl.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuInstallerServiceImpl.kt @@ -20,11 +20,7 @@ class DhizukuInstallerServiceImpl() : IDhizukuInstallerService.Stub() { private const val STATUS_SUCCESS = 0 private const val STATUS_FAILURE = -1 - // Surfaced to the AIDL caller (SilentInstallerDispatcher) so it can - // distinguish "Android 14+ update-ownership demanded user confirm" - // from a generic failure — the dispatcher uses this to retry the - // install without installerAttribution before falling back to the - // system installer dialog. + const val STATUS_PENDING_USER_ACTION_REQUIRED = -2 private const val INSTALL_TIMEOUT_SECONDS = 120L private const val UNINSTALL_TIMEOUT_SECONDS = 30L @@ -100,13 +96,7 @@ class DhizukuInstallerServiceImpl() : IDhizukuInstallerService.Stub() { when (status) { PackageInstaller.STATUS_SUCCESS -> resultRef.set(STATUS_SUCCESS) PackageInstaller.STATUS_PENDING_USER_ACTION -> { - // Android 14+ update-ownership enforcement: even - // with USER_ACTION_NOT_REQUIRED + DPC privilege, - // setting `installerPackageName` to a value that - // doesn't match the existing installer of record - // makes the platform demand user confirmation. - // Surface as a distinct error so the dispatcher - // can retry without attribution or fall back. + logW("install requires user action (Android 14+ update-ownership gate) — installerAttribution=$installerPackageName") resultRef.set(STATUS_PENDING_USER_ACTION_REQUIRED) } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuServiceManager.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuServiceManager.kt index deda4ee50..72e1c9606 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuServiceManager.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/dhizuku/DhizukuServiceManager.kt @@ -34,12 +34,7 @@ class DhizukuServiceManager( private set fun initialize() { - // Eagerly call `Dhizuku.init(context)` here so the underlying binder - // is warmed up before the first install attempt. Without this, - // `refreshStatus()` racing the install dispatch can observe a - // not-yet-connected binder and report NOT_RUNNING, making the - // dispatcher fall back to the system installer dialog even though - // Dhizuku is set up and ready. + runCatching { Dhizuku.init(context) } .onFailure { Logger.w(TAG) { "warm-up Dhizuku.init() threw: ${it.message}" } } refreshStatus() diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/AndroidExternalAppScanner.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/AndroidExternalAppScanner.kt index eb55cee2e..b8001c43a 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/AndroidExternalAppScanner.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/AndroidExternalAppScanner.kt @@ -22,14 +22,10 @@ class AndroidExternalAppScanner( override suspend fun isPermissionGranted(): Boolean = withContext(Dispatchers.IO) { - // Android 11+: getInstalledPackages without QUERY_ALL_PACKAGES returns - // only the self-visible subset. We treat "saw something other than self - // and the declared packages" as a proxy for grant. - // On API < 30 the permission is not enforced — always granted. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return@withContext true val pkgs = listInstalledPackages(includeMeta = false) - // Heuristic: a typical device has 50+ visible packages. With QUERY_ALL_PACKAGES - // denied + our block, we usually see ~5-10. Treat >= 30 visible as granted. + pkgs.size >= GRANT_THRESHOLD } @@ -41,8 +37,7 @@ class AndroidExternalAppScanner( if (granted) { 0 } else { - // soft "we probably can't see ~150 more" estimate; the scanner UI - // marks this as approximate. + INVISIBLE_GUESS } VisiblePackageEstimate( @@ -76,8 +71,7 @@ class AndroidExternalAppScanner( private fun listInstalledPackages(includeMeta: Boolean): List { val baseFlags = if (includeMeta) PackageManager.GET_META_DATA.toLong() else 0L - // We deliberately do NOT request signing certificates here — we compute - // the fingerprint per-package on demand to keep the bulk listing cheap. + return runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(baseFlags)) diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/InstallerSourceClassifier.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/InstallerSourceClassifier.kt index 0bf314e7b..c69b4eaff 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/InstallerSourceClassifier.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/InstallerSourceClassifier.kt @@ -18,9 +18,6 @@ class InstallerSourceClassifier( val flags = applicationInfo?.flags ?: 0 val isSystem = flags and ApplicationInfo.FLAG_SYSTEM != 0 - // FLAG_SYSTEM apps that received an OTA update get FLAG_UPDATED_SYSTEM_APP added - // *without* losing FLAG_SYSTEM. Treat any FLAG_SYSTEM app as SYSTEM regardless of - // update status — Samsung / Pixel / OEM-bundled apps almost never come from GitHub. if (isSystem) return InstallerKind.SYSTEM if (OEM_PACKAGE_PREFIXES.any { packageName.startsWith(it) }) return InstallerKind.STORE_OEM_OTHER @@ -107,9 +104,6 @@ class InstallerSourceClassifier( "com.google.android.packageinstaller", ) - // Catch-all for OEM apps that lost FLAG_SYSTEM after a self-update or - // were preloaded as not-quite-system (Samsung does both). These are - // never GitHub-published; the wizard surfacing them is just noise. private val OEM_PACKAGE_PREFIXES = listOf( "com.samsung.", diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/ManifestHintExtractor.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/ManifestHintExtractor.kt index eae19479f..b5ff10a05 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/ManifestHintExtractor.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/ManifestHintExtractor.kt @@ -71,7 +71,6 @@ class ManifestHintExtractor { val confidence: Double, ) - // declaration order = priority order private val URL_KEYS = listOf( UrlKey(KEY_FDROID_SOURCE_CODE, ManifestHintSource.META_FDROID_SOURCE_CODE, 0.85), @@ -83,7 +82,6 @@ class ManifestHintExtractor { private val OWNER_REGEX = Regex("^[\\w.-]{1,39}$") private val REPO_NAME_REGEX = Regex("^[\\w.-]{1,100}$") - // Matches https?://github.com//(/...)? — first two path segments only private val URL_REGEX = Regex( "(?i)^https?://(?:www\\.)?github\\.com/([\\w.-]+)/([\\w.-]+)(?:[/?#].*)?$", diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/SigningFingerprintComputer.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/SigningFingerprintComputer.kt index 07cee9b15..bec79c9cb 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/SigningFingerprintComputer.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/external/SigningFingerprintComputer.kt @@ -45,10 +45,7 @@ object SigningFingerprintComputer { private fun certBytes(info: PackageInfo): ByteArray? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val sigInfo = info.signingInfo ?: return null - // Prefer the active signer set. signingCertificateHistory is ordered with - // the original cert at index 0 and the current cert at the last index, so - // .firstOrNull() would return the pre-rotation cert for v3-rotated apps - // and break fingerprint matching against signed binaries. + val current = sigInfo.apkContentsSigners?.firstOrNull() val fallback = sigInfo.signingCertificateHistory?.lastOrNull() (current ?: fallback)?.toByteArray() diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/installer/SilentInstallerDispatcher.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/installer/SilentInstallerDispatcher.kt index bba46f021..be4db240c 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/installer/SilentInstallerDispatcher.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/installer/SilentInstallerDispatcher.kt @@ -141,11 +141,7 @@ class SilentInstallerDispatcher( withContext(Dispatchers.IO) { runInstall(file, backend, installerAttribution, expectedPkg, expectedVc) } - // Android 14+ may reject a session that carries a non-matching - // installerPackageName with STATUS_PENDING_USER_ACTION even when - // USER_ACTION_NOT_REQUIRED is set (update-ownership enforcement). - // Retry once without attribution — the install stays silent, the - // app just loses its "installed by Play Store / F-Droid" label. + val resolved = if ( backend == Backend.DHIZUKU && @@ -197,15 +193,11 @@ class SilentInstallerDispatcher( service.installPackage(pfd, file.length(), expectedPkg, expectedVc, installerAttribution) } Backend.ROOT -> - // Root path streams the APK directly into `pm install`'s - // stdin from the app's own process, so no ParcelFileDescriptor - // dance is needed — the file lives on app-private storage and - // we read it back via the standard FileInputStream. + rootServiceManager.installPackage(file, installerAttribution) Backend.DEFAULT -> null } - private fun readApkIdentity(filePath: String): Pair { val info = try { androidContext.packageManager.getPackageArchiveInfo(filePath, 0) @@ -256,13 +248,7 @@ class SilentInstallerDispatcher( if (dhizukuServiceManager.status.value == DhizukuStatus.READY) Backend.DHIZUKU else Backend.DEFAULT } InstallerType.ROOT -> { - // Root grants are sticky once accepted by Magisk/KernelSU/APatch, - // so a READY cached status is trustworthy on the hot path. When - // the user is still on PERMISSION_NEEDED, re-probe (async) so a - // fresh grant since app start can flip the status without forcing - // them back to Tweaks. The probe shells out and is slow; gating - // it on `!= READY` keeps install latency unaffected once root has - // been granted once. + if (rootServiceManager.status.value != RootStatus.READY) { rootServiceManager.refreshStatus() } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/root/RootServiceManager.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/root/RootServiceManager.kt index 8cc7168c3..195c66e86 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/root/RootServiceManager.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/root/RootServiceManager.kt @@ -25,10 +25,7 @@ class RootServiceManager( private var cachedSuPath: String? = null fun initialize() { - // First-launch detection runs off the main thread because `su -c id` - // can block for several seconds when the root manager (Magisk / - // KernelSU / APatch) is showing its grant dialog or doing initial - // bookkeeping. We don't want to stall app cold-start. + scope.launch(Dispatchers.IO) { refreshStatusBlocking() } @@ -38,16 +35,6 @@ class RootServiceManager( scope.launch(Dispatchers.IO) { refreshStatusBlocking() } } - /** - * Triggers the root manager's per-app authorization dialog without - * actually performing a privileged action. Idiomatic across Magisk / - * KernelSU / APatch — running `su -c true` is enough to make the - * manager pop its allow/deny prompt; the grant is then cached and - * subsequent calls run silently. - * - * No-op when the su binary isn't on the device. Re-runs status - * detection afterwards so the UI reflects the user's choice. - */ fun requestPermission() { scope.launch(Dispatchers.IO) { val su = cachedSuPath ?: locateSuBinary()?.path ?: run { @@ -70,20 +57,6 @@ class RootServiceManager( } } - /** - * Pipes [apkFile] into `pm install -S -i -` over `su`. - * - * Stdin streaming is deliberate: invoking `pm install ` against - * an app-private APK location fails because the shell process — even - * running as UID 0 — is denied by SELinux on `app_data_files`. Reading - * the bytes from app process and writing them to `pm`'s stdin sidesteps - * the policy. - * - * Returns `0` on success, non-zero on failure, `null` when no su binary - * is reachable. Mirrors the AIDL contract of the Shizuku / Dhizuku - * services so [SilentInstallerDispatcher] can treat all silent backends - * uniformly. - */ suspend fun installPackage( apkFile: File, installerPackageName: String?, @@ -93,10 +66,7 @@ class RootServiceManager( Logger.w(TAG) { "installPackage() — no su binary available" } return@withContext null } - // The whole string gets injected into a `su -c ''` shell — - // refuse anything that isn't a strictly conformant Android - // package name so a malformed user-supplied installer - // attribution can't smuggle shell metacharacters past us. + val safeInstaller = installerPackageName?.takeIf { it.isNotBlank() } if (safeInstaller != null && !PACKAGE_NAME_PATTERN.matches(safeInstaller)) { Logger.w(TAG) { @@ -105,10 +75,7 @@ class RootServiceManager( return@withContext STATUS_FAILURE } val pm = "/system/bin/pm" - // Always shell out the full pm path — some Magisk modules / - // KernelSU configurations strip `/system/bin` from the minimal - // PATH that `su -c ` runs against, and the resulting - // `pm: not found` (exit 127) is a silent class of bug. + val command = buildString { append(pm).append(" install ") if (safeInstaller != null) append("-i ").append(safeInstaller).append(' ') @@ -123,12 +90,6 @@ class RootServiceManager( return@withContext null } - // Drain stdout and stderr concurrently with the stdin pump - // because `readText()` blocks until EOF — if pm keeps the pipes - // open while waiting for stdin, calling `readText()` BEFORE - // `waitFor()` deadlocks the process and the install timeout - // never fires (the read just hangs forever inside the read - // syscall). val stdoutBuf = StringBuilder() val stderrBuf = StringBuilder() val stdoutThread = Thread { @@ -182,9 +143,7 @@ class RootServiceManager( stderrThread.join(READER_DRAIN_TIMEOUT_MS) return@withContext STATUS_FAILURE } - // Reader threads will see EOF now that the process is gone; - // wait for them with a short cap so a stuck pipe never blocks - // us indefinitely. + stdoutThread.join(READER_DRAIN_TIMEOUT_MS) stderrThread.join(READER_DRAIN_TIMEOUT_MS) @@ -260,25 +219,8 @@ class RootServiceManager( } } - /** - * Locate a working `su` binary by exec-probing each candidate. - * - * Why exec-probe instead of `File.exists()`: on Android 13+ the - * `untrusted_app` SELinux profile blocks stat'ing `/data/adb/...` - * paths, so `File("/data/adb/magisk/su").exists()` returns false - * even when Magisk is installed and would grant root. Magisk / - * KernelSU / APatch hook the exec syscall regardless of whether - * the app can stat the path, so a direct exec is the only reliable - * probe on modern Android. - */ private fun locateSuBinary(): SuProbe? { - // A device can have multiple su binaries — e.g. a ROM-baked - // `/system/bin/su` that's denied alongside a Magisk install at - // `/data/adb/magisk/su` that would grant. Always scan all paths; - // prefer a confirmed UID_ZERO over a NOT_ZERO so the first denied - // candidate doesn't shadow a working one. Fall back to the first - // NOT_ZERO so the grant-prompt path still surfaces when none of - // the binaries is currently granted. + var firstNotZero: SuProbe? = null for (path in SU_PATHS) { val result = probeSu(path) ?: continue @@ -312,9 +254,7 @@ class RootServiceManager( } } } catch (_: java.io.IOException) { - // exec failed — binary doesn't exist at this path on this device. - // Skip to next candidate. Not logged at warn level because this is - // the expected outcome for most paths in the SU_PATHS list. + null } catch (e: Exception) { Logger.w(TAG) { "probeSu($path) threw: ${e.javaClass.simpleName}: ${e.message}" } @@ -341,9 +281,6 @@ class RootServiceManager( private const val PROMPT_TIMEOUT_SECONDS = 60L private const val READER_DRAIN_TIMEOUT_MS = 1_000L - // Strict Android package-name shape — letters, digits, underscores - // separated by dots, must start with a letter. Used to reject - // anything that could carry shell metacharacters into `su -c`. private val PACKAGE_NAME_PATTERN = Regex("""^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$""") diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt index c2225fd90..a6062cff9 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt @@ -5,19 +5,6 @@ import java.io.BufferedReader import java.io.InputStreamReader import java.util.concurrent.TimeUnit -/** - * Shizuku UserService implementation that runs in a privileged process (shell/root). - * Provides silent package install/uninstall via `pm` shell commands. - * - * This class runs in Shizuku's process, NOT in the app's process. - * It has shell-level (UID 2000) or root-level (UID 0) privileges. - * - * Uses `pm install` with stdin pipe for install, `pm uninstall` for uninstall. - * This is the most reliable approach — avoids fragile reflection on hidden - * IPackageInstaller/IPackageInstallerSession/IIntentSender APIs. - * - * MUST have a default no-arg constructor for Shizuku's UserService framework. - */ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() { companion object { @@ -52,7 +39,6 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() { val process = Runtime.getRuntime().exec(command) - // Pipe the APK from the ParcelFileDescriptor to pm's stdin val writeThread = Thread { try { ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input -> @@ -74,7 +60,6 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() { } writeThread.start() - // Read stdout/stderr for result val stdout = BufferedReader(InputStreamReader(process.inputStream)).readText().trim() val stderr = BufferedReader(InputStreamReader(process.errorStream)).readText().trim() diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidShareManager.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidShareManager.kt index 888537ae5..7f884f6f9 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidShareManager.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidShareManager.kt @@ -97,7 +97,7 @@ class AndroidShareManager( } launcher.launch(intent) } else { - // Fallback: try with ACTION_GET_CONTENT + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = mimeType @@ -107,7 +107,7 @@ class AndroidShareManager( context.startActivity(Intent.createChooser(intent, null).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) - // Note: fallback won't deliver result without launcher + onResult(null) } catch (e: Exception) { onResult(null) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/impl/DefaultTokenStore.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/impl/DefaultTokenStore.kt index 8316bdf16..6b52a0fd3 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/impl/DefaultTokenStore.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/data_source/impl/DefaultTokenStore.kt @@ -70,11 +70,7 @@ class DefaultTokenStore( override suspend fun clear() { migrationDeferred.await() - // safeDelete swallows KSafe failures (returns false). Legacy cleanup - // runs unconditionally so a sign-out never leaves a stale plaintext - // token. If KSafe delete silently failed, the encrypted token row - // remains but is unreachable without a re-issued auth — acceptable - // trade vs crashing the sign-out flow. + ksafe.safeDelete(tokenKey) runCatching { legacyDataStore.edit { it.remove(legacyKey) } } } @@ -100,16 +96,13 @@ class DefaultTokenStore( val parsed = runCatching { json.decodeFromString(GithubDeviceTokenSuccessDto.serializer(), legacyRaw) }.getOrNull() ?: run { - // Legacy value present but unparseable — leave it in DataStore - // (don't drop the only copy) and bail out without marking - // migrated, so a code change that fixes the parse can retry. + return@withLock } val putOk = ksafe.safePut(tokenKey, parsed) if (!putOk) { - // Never drop the legacy entry if we couldn't persist into KSafe. - // The user's token stays readable via the slow path next launch. + return@withLock } runCatching { legacyDataStore.edit { it.remove(legacyKey) } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index 83e433285..bdbbdf68f 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout -import named import org.koin.core.qualifier.named import org.koin.dsl.module import zed.rainxch.core.data.network.createPlatformHttpClient @@ -57,7 +56,6 @@ import zed.rainxch.core.data.repository.FavouritesRepositoryImpl import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl import zed.rainxch.core.data.repository.ProxyRepositoryImpl import zed.rainxch.core.data.repository.RateLimitRepositoryImpl -import zed.rainxch.core.data.repository.SearchHistoryRepositoryImpl import zed.rainxch.core.data.repository.HiddenReposRepositoryImpl import zed.rainxch.core.data.repository.SeenReposRepositoryImpl import zed.rainxch.core.data.repository.StarredRepositoryImpl @@ -85,7 +83,6 @@ import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.MirrorRepository import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.RateLimitRepository -import zed.rainxch.core.domain.repository.SearchHistoryRepository import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/MultiSourceDownloaderImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/MultiSourceDownloaderImpl.kt index 70945f198..f37a07011 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/MultiSourceDownloaderImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/MultiSourceDownloaderImpl.kt @@ -24,9 +24,6 @@ class MultiSourceDownloaderImpl( suggestedFileName: String?, ): Flow { val active = ProxyManager.currentMirror() - // The multi-source race targets release-asset downloads. A mirror that - // doesn't list RELEASE_ASSET (e.g. jsDelivr, raw-files only) can't - // serve these URLs — fall through to a Direct download. if (active == null || TrafficKind.RELEASE_ASSET !in active.trafficKinds) { return downloader.download(githubUrl, suggestedFileName) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/SlowDownloadDetectorImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/SlowDownloadDetectorImpl.kt index f0e85fa7f..3332b4c16 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/SlowDownloadDetectorImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/download/SlowDownloadDetectorImpl.kt @@ -70,10 +70,6 @@ class SlowDownloadDetectorImpl( val active = ProxyManager.currentMirror() if (active != null && TrafficKind.RELEASE_ASSET in active.trafficKinds) return - // Mirror's KSafe migration runs on init in MirrorRepositoryImpl. Until - // it flips this marker, K_SUGGEST_* read as defaults — which would - // re-prompt users who'd already dismissed permanently. Bail out - // silently until migration is observed complete. val migrationDone = runCatching { ksafe.safeGet(MIRROR_MIGRATION_MARKER, false) }.getOrDefault(false) if (!migrationDone) return @@ -87,8 +83,6 @@ class SlowDownloadDetectorImpl( } private companion object { - // Same keys MirrorRepositoryImpl writes — shared across both files - // to avoid coupling on a constants object that no longer exists. const val K_SUGGEST_DISMISSED = "mirror_auto_suggest_dismissed" const val K_SUGGEST_SNOOZE = "mirror_auto_suggest_snooze_until" const val MIRROR_MIGRATION_MARKER = "__migrated_mirror_v1__" diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt index 1f5f90015..4192e0685 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/AssetNetwork.kt @@ -7,9 +7,7 @@ import kotlinx.serialization.Serializable data class AssetNetwork( @SerialName("id") val id: Long, @SerialName("name") val name: String, - // Optional: GitHub always emits this, Forgejo / Codeberg / Gitea - // releases payload omits it entirely. Defaulting to null lets the - // shared DTO deserialize against both shapes. + @SerialName("content_type") val contentType: String? = null, @SerialName("size") val size: Long, @SerialName("browser_download_url") val downloadUrl: String, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubReadmeResponseDto.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubReadmeResponseDto.kt index ec49711e5..d6ff640a2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubReadmeResponseDto.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubReadmeResponseDto.kt @@ -7,10 +7,7 @@ import kotlinx.serialization.Serializable data class GithubReadmeResponseDto( @SerialName("name") val name: String? = null, @SerialName("path") val path: String? = null, - // Directory-listing entries (Forgejo `/contents/` with no filepath) - // have no `content` of their own — the field is omitted. Make it - // nullable so the same DTO can be reused for both single-file and - // listing responses. + @SerialName("content") val content: String? = null, @SerialName("encoding") val encoding: String? = null, @SerialName("type") val type: String? = null, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/SigningFingerprintSeedResponse.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/SigningFingerprintSeedResponse.kt index 7232fb602..a969e76b4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/SigningFingerprintSeedResponse.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/SigningFingerprintSeedResponse.kt @@ -12,10 +12,7 @@ data class SigningFingerprintSeedResponse( val fingerprint: String, val owner: String, val repo: String, - // Epoch milliseconds. Backend contract (E1 plan §7.4); the same value is - // forwarded as `since` on the next sync — any unit drift between client - // and backend would show up as either re-fetched pages or a hard skip, - // which the seed-sync telemetry will surface. + val observedAt: Long, ) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt index 22374565b..7a608f1e1 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/CacheDao.kt @@ -29,13 +29,6 @@ interface CacheDao { @Query("DELETE FROM cache_entries WHERE `key` = :key") suspend fun delete(key: String) - /** - * Conditional delete used by CacheManager when a row decoded as a - * malformed payload — guards against evicting a freshly-put row that - * raced in between the read and the decode-failure delete. Compares - * `cachedAt` because (key, cachedAt) is effectively the row's version - * stamp on this schema. - */ @Query("DELETE FROM cache_entries WHERE `key` = :key AND cachedAt = :cachedAt") suspend fun deleteIfMatches(key: String, cachedAt: Long) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/FavoriteRepoDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/FavoriteRepoDao.kt index c88ae6cf1..876a0ccd2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/FavoriteRepoDao.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/FavoriteRepoDao.kt @@ -33,9 +33,9 @@ interface FavoriteRepoDao { @Query( """ - UPDATE favorite_repos - SET isInstalled = :installed, - installedPackageName = :packageName + UPDATE favorite_repos + SET isInstalled = :installed, + installedPackageName = :packageName WHERE repoId = :repoId """, ) @@ -47,7 +47,7 @@ interface FavoriteRepoDao { @Query( """ - UPDATE favorite_repos + UPDATE favorite_repos SET latestVersion = :version, latestReleaseUrl = :releaseUrl, lastSyncedAt = :timestamp diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt index ff33fd8af..597818bf9 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt @@ -49,8 +49,8 @@ interface InstalledAppDao { @Query( """ - UPDATE installed_apps - SET isUpdateAvailable = :available, + UPDATE installed_apps + SET isUpdateAvailable = :available, latestVersion = :version, latestAssetName = :assetName, latestAssetUrl = :assetUrl, @@ -97,16 +97,6 @@ interface InstalledAppDao { fallback: Boolean, ) - /** - * Sets the user's preferred asset variant along with its multi-layer - * fingerprint (token set, glob pattern, same-position metadata). - * Always clears the "stale" flag in the same write because the user - * has just made an explicit choice — whatever was stored before is - * no longer stale, even if the new variant is the same value. - * - * Pass `null` for [variant] (and the other fingerprint fields) to - * unpin and fall back to the platform auto-picker. - */ @Query( """ UPDATE installed_apps @@ -128,11 +118,6 @@ interface InstalledAppDao { siblingCount: Int?, ) - /** - * Sets `preferredVariantStale` for [packageName]. Used by - * `checkForUpdates` when the persisted variant cannot be matched - * against the assets in a fresh release. - */ @Query( """ UPDATE installed_apps @@ -151,17 +136,6 @@ interface InstalledAppDao { timestamp: Long, ) - /** - * Sets (or clears) [InstalledAppEntity.skippedReleaseTag] for - * [packageName]. Pair-writes `isUpdateAvailable = 0` when [tag] is - * non-null so the apps list drops the badge atomically with the - * skip — without the second column the UI would still show "Update" - * for a frame until the next periodic check ran. - * - * Pass `null` for [tag] to unskip; the row's update badge stays - * untouched in that branch and the next `checkForUpdates` will - * recompute it normally. - */ @Query( """ UPDATE installed_apps @@ -184,14 +158,6 @@ interface InstalledAppDao { ) fun getAppsWithSkippedReleaseTag(): Flow> - /** - * Atomically writes the installed-version columns and the - * `isUpdateAvailable` flag for [packageName]. Used by the external - * install / sideload path (`PackageEventReceiver.handleExternalInstall`) - * where a stale snapshot + full-row update could otherwise clobber - * concurrent writes to sibling columns (download orchestrator, - * variant pin, favourite toggle, `checkForUpdates`, etc.). - */ @Query( """ UPDATE installed_apps @@ -210,15 +176,6 @@ interface InstalledAppDao { isUpdateAvailable: Boolean, ) - /** - * Sets the path + version + asset name of a - * downloaded-but-not-yet-installed asset. Pass all `null` to - * clear (e.g. after the user installs the file). - * - * The version + asset name pair is what the Details screen uses - * to detect "the parked file matches the currently-selected - * release" and skip the redundant re-download. - */ @Query( """ UPDATE installed_apps @@ -235,14 +192,6 @@ interface InstalledAppDao { assetName: String?, ) - /** - * Atomically clears the "update available" badge and any cached - * latest-release metadata for [packageName], while bumping - * `lastCheckedAt`. Used by `checkForUpdates` whenever the current - * filter / release window yields no match: without this, a user who - * tightens their asset filter would keep the stale badge and the - * download button would point at an asset that no longer matches. - */ @Query( """ UPDATE installed_apps diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/StarredRepoDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/StarredRepoDao.kt index 13f0cc2ee..fbdaa8c28 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/StarredRepoDao.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/StarredRepoDao.kt @@ -39,9 +39,9 @@ interface StarredRepoDao { @Query( """ - UPDATE starred_repos - SET isInstalled = :installed, - installedPackageName = :packageName + UPDATE starred_repos + SET isInstalled = :installed, + installedPackageName = :packageName WHERE repoId = :repoId """, ) @@ -53,7 +53,7 @@ interface StarredRepoDao { @Query( """ - UPDATE starred_repos + UPDATE starred_repos SET latestVersion = :version, latestReleaseUrl = :releaseUrl, lastSyncedAt = :timestamp diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt index 9a5251792..cc02cb679 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt @@ -40,131 +40,31 @@ data class InstalledAppEntity( val latestVersionCode: Long? = null, val latestReleasePublishedAt: String? = null, val includePreReleases: Boolean = false, - /** - * Per-app regex applied to asset (file) names. When non-null, only assets - * whose name matches the pattern are considered installable for this app. - * Used to track a single app inside a monorepo that ships multiple apps - * (e.g. `ente-auth.*` against `ente-io/ente`). - */ + val assetFilterRegex: String? = null, - /** - * When true, the update checker walks backward through past releases until - * it finds one whose assets match [assetFilterRegex]. Required for - * monorepos where the latest release is for a *different* app. - * - * `@ColumnInfo(defaultValue = "0")` matches `MIGRATION_9_10`'s - * `DEFAULT 0` clause so Room's schema validator doesn't flag the - * column on freshly-built databases. - */ + @ColumnInfo(defaultValue = "0") val fallbackToOlderReleases: Boolean = false, - /** - * Stable identifier for the asset variant the user wants to track — - * for example `arm64-v8a`, `universal`, or `x86_64`. Derived from the - * picked asset filename by stripping the version segment, so it - * survives release-to-release version bumps. - * - * `null` means "use the platform installer's automatic picker" - * (today's behaviour). When non-null, [checkForUpdates] resolves the - * matching asset on every check; if no asset in the new release - * matches the variant, [preferredVariantStale] is flipped to true. - */ + val preferredAssetVariant: String? = null, - /** - * Set to true by the update checker when the persisted - * [preferredAssetVariant] cannot be found in the latest release's - * assets — typically because the maintainer renamed or restructured - * the artefacts. The UI surfaces this with a "variant changed — - * pick again" prompt and clears it once the user picks a new variant. - * - * `@ColumnInfo(defaultValue = "0")` matches `MIGRATION_10_11`'s - * `DEFAULT 0` clause so Room's schema validator doesn't flag a - * mismatch between the migrated table and the freshly-created one. - */ + @ColumnInfo(defaultValue = "0") val preferredVariantStale: Boolean = false, - /** - * Token-set fingerprint of the picked asset, serialized as - * `token1|token2|…` (alphabetically sorted so equal sets always - * serialize to identical strings — letting the resolver compare - * with plain string equality instead of parsing). - * - * The token vocabulary is closed (see `AssetVariant.kt`); only - * recognised arch / flavor tokens are stored. `null` means - * "no token fingerprint available — fall back to glob or tail". - */ + val preferredAssetTokens: String? = null, - /** - * Glob-pattern fingerprint of the picked asset (e.g. - * `app-*-arm64-v8a.apk`). Used as a secondary identity layer when - * the token vocabulary doesn't recognise anything in the filename - * — the most common case being custom flavor names. - * - * `null` means the picked filename had no version-shaped substring - * to wildcard, so the glob would just equal the filename and - * provides no rescue value. - */ + val assetGlobPattern: String? = null, - /** - * Zero-based index of the picked asset in the original release's - * installable-asset list. Used by the same-position fallback — - * when none of the fingerprint layers match in a fresh release - * but the new release has exactly the same number of installable - * assets, the asset at this index is preferred. - * - * Weak signal, only consulted as a last resort. `null` for older - * rows pinned before this column existed. - */ + val pickedAssetIndex: Int? = null, - /** - * Total installable assets in the release the user picked from. - * Pairs with [pickedAssetIndex] for same-position fallback. - */ + val pickedAssetSiblingCount: Int? = null, - /** - * Absolute path to a downloaded but not-yet-installed APK / asset. - * - * Set by `DefaultDownloadOrchestrator` when an - * `InstallPolicy.InstallWhileForeground` download finishes after - * the foreground screen has been destroyed (the user navigated - * away mid-download). The apps list shows a "Ready to install" - * row with a one-tap install action when this is non-null. - * - * Cleared by the orchestrator on successful install or by the - * user dismissing the row. - * - * Survives process restarts: the file lives on disk and the - * column points back at it. Both are cleaned up if the file is - * missing on next read. - */ + val pendingInstallFilePath: String? = null, - /** - * Release tag of the version represented by [pendingInstallFilePath]. - * Used by the Details screen to detect "the parked file matches - * the currently-selected release" — when both [pendingInstallVersion] - * and [pendingInstallAssetName] match the user's selection, the - * install button skips the download phase and dispatches the - * existing install dialog flow on the parked file directly. - */ + val pendingInstallVersion: String? = null, - /** - * Asset filename (the original, not the scoped form) of the - * parked file. Pairs with [pendingInstallVersion] for - * Details-screen "ready to install" detection. - */ + val pendingInstallAssetName: String? = null, - /** - * Tag of an upstream release the user has chosen to ignore. Set when - * the user taps "Skip this version" on the apps row. While non-null - * and equal to the matched release tag, [checkForUpdates] suppresses - * the update badge for [packageName]. The flag auto-clears the - * moment a strictly newer release lands so the user gets re-notified - * the next cycle without having to unskip manually. - * - * `@ColumnInfo(defaultValue = "NULL")` matches `MIGRATION_15_16` so - * Room's schema validator doesn't flag a mismatch between the - * migrated table and the freshly-created one. - */ + @ColumnInfo(defaultValue = "NULL") val skippedReleaseTag: String? = null, @ColumnInfo(defaultValue = "NULL") diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt index 0d8c46ac5..12814d3a2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/AssetNetwork.kt @@ -8,9 +8,7 @@ fun AssetNetwork.toDomain(): GithubAsset = GithubAsset( id = id, name = name, - // Forgejo / Codeberg omits `content_type` on release assets; - // fall back to a reasonable default so installer / MIME-based - // classifiers don't NPE. + contentType = contentType ?: "application/octet-stream", size = size, downloadUrl = downloadUrl, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt index c9088f6e2..00298afd7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt @@ -47,12 +47,6 @@ import zed.rainxch.core.data.dto.UserProfileNetwork import zed.rainxch.core.domain.model.ProxyConfig import kotlin.coroutines.cancellation.CancellationException -/** - * Client for GitHub Store's own backend (trending/popular/search). - * Treated as *discovery* traffic — routes through the discovery-scope - * proxy so users configuring a proxy for GitHub browsing also have - * their backend discovery requests proxied consistently. - */ class BackendApiClient( proxyConfigFlow: StateFlow, private val tokenStore: TokenStore, @@ -223,7 +217,6 @@ class BackendApiClient( parameter("page", page) parameter("per_page", perPage) if (token != null) header(X_GITHUB_TOKEN_HEADER, token) - // Cold path: backend goes to GitHub + paginates. 15s covers p99. timeout { requestTimeoutMillis = 15_000 socketTimeoutMillis = 15_000 @@ -283,29 +276,6 @@ class BackendApiClient( } } - suspend fun getUserStarred( - username: String, - page: Int = 1, - perPage: Int = 30, - ): Result> = - safeCall { - val token = currentUserGithubToken() - val response = httpClient.get("users/$username/starred") { - parameter("page", page) - parameter("per_page", perPage) - if (token != null) header(X_GITHUB_TOKEN_HEADER, token) - timeout { - requestTimeoutMillis = 15_000 - socketTimeoutMillis = 15_000 - } - } - when { - response.status.isSuccess() -> Result.success(response.body()) - response.status == HttpStatusCode.TooManyRequests -> Result.failure(buildRateLimited(response)) - else -> Result.failure(BackendException(response.status.value)) - } - } - suspend fun getUser(username: String): Result = safeCall { val token = currentUserGithubToken() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendFallbackPolicy.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendFallbackPolicy.kt index 291d9d805..015b6bae3 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendFallbackPolicy.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendFallbackPolicy.kt @@ -5,12 +5,6 @@ import zed.rainxch.core.domain.model.RateLimitInfo import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Clock -/** - * Centralized policy for backend → direct-GitHub fallback. **Side - * effect:** rethrows `CancellationException` so structured concurrency - * is preserved, and converts a backend rate-limit into a domain - * [RateLimitException] so callers can surface retry-after to the user. - */ fun shouldFallbackToGithubOrRethrow(cause: Throwable): Boolean = when (cause) { is CancellationException -> throw cause diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ExternalMatchApi.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ExternalMatchApi.kt index 72f354ec9..4b677e6d5 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ExternalMatchApi.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ExternalMatchApi.kt @@ -8,7 +8,6 @@ import zed.rainxch.core.data.dto.ExternalMatchResponse import zed.rainxch.core.domain.repository.TweaksRepository interface ExternalMatchApi { - // NOTE: chunking lives in the repo; impls receive whatever the repo passes suspend fun match(request: ExternalMatchRequest): Result } @@ -54,10 +53,12 @@ class ExternalMatchApiSelector( tweaks: TweaksRepository, scope: CoroutineScope, ) : ExternalMatchApi { - // Cache the flag in a hot StateFlow so `match()` can read it - // synchronously instead of round-tripping DataStore on every call. private val flagState = tweaks.getExternalMatchSearchEnabled() - .stateIn(scope, SharingStarted.Eagerly, initialValue = false) + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = false + ) override suspend fun match(request: ExternalMatchRequest): Result = if (flagState.value) { diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoApiClient.kt index d3037c307..0b59f6af7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoApiClient.kt @@ -23,8 +23,8 @@ import zed.rainxch.core.domain.model.ProxyConfig import java.io.IOException class ForgejoApiClient( - private val host: String, - private val proxyConfig: ProxyConfig = ProxyConfig.System, + host: String, + proxyConfig: ProxyConfig = ProxyConfig.System, ) { private val baseUrl: String = "https://$host/api/v1" @@ -92,20 +92,6 @@ class ForgejoApiClient( } } - /** - * Forgejo does **not** expose GitHub's `/readme` convenience - * endpoint (verified live against codeberg.org — 404 across every - * repo). Use the `/contents/{filepath}` route, which returns the - * exact same `{name, path, content, encoding}` shape that the - * GitHub readme decoder already handles. The README filename has - * to be supplied explicitly; callers typically try `README.md` - * first and fall back to listing `/contents/` for `^README(\..+)?$` - * matches when that 404s. - * - * `ref` defaults to the repo's default branch when omitted, but we - * pass it explicitly so newly-created repos with a non-`main` HEAD - * resolve correctly. - */ suspend fun getContentsFile( owner: String, repo: String, @@ -129,13 +115,6 @@ class ForgejoApiClient( } } - /** - * Release the underlying Ktor HttpClient + its engine. Called by - * [ForgejoClientRegistry] when the registry shuts down OR when the - * cached client is invalidated (proxy config change). Without this - * the engine's connection pool + dispatcher stay alive for the rest - * of the process. - */ fun close() { runCatching { client.close() } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoClientRegistry.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoClientRegistry.kt index 726274a3f..f64c4738f 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoClientRegistry.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ForgejoClientRegistry.kt @@ -25,10 +25,6 @@ class ForgejoClientRegistry( .drop(1) .distinctUntilChanged() .onEach { _ -> - // Invalidate cached clients so the next clientFor() - // rebuilds them against the new proxy config. Close - // each before dropping the reference — otherwise their - // engines, dispatchers, and connection pools leak. mutex.withLock { clients.values.forEach { it.close() } clients.clear() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt index 678f04032..2d2b4dcc2 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt @@ -41,9 +41,6 @@ class GitHubClientProvider( .distinctUntilChanged() .onEach { proxyConfig -> mutex.withLock { - // Replace-then-close: readers of [client] always see - // a live client. Closing first opens a window where - // an in-flight call could touch a closed engine. val replacement = createGitHubHttpClient( tokenStore = tokenStore, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorApiClient.kt index 89f7ce500..1833b8ae4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorApiClient.kt @@ -5,12 +5,6 @@ import zed.rainxch.core.data.dto.MirrorListResponse class MirrorApiClient( private val backendApiClient: BackendApiClient, ) { - /** - * Fetches the mirror catalog from the backend. Delegates to - * [BackendApiClient] — which routes through the discovery proxy - * scope but never through MirrorRewriteInterceptor (the - * interceptor lives on the GitHub-bound client only). - */ suspend fun fetchList(): Result = backendApiClient.getMirrorList() } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriteInterceptor.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriteInterceptor.kt index ffa8f2011..e0afc061c 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriteInterceptor.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriteInterceptor.kt @@ -25,8 +25,6 @@ fun HttpClient.installMirrorRewrite() { if (rewritten != null) { val originalHost = request.url.host request.url.takeFrom(rewritten) - // Host changed — strip the user's GitHub bearer token so we - // never send it to a community mirror. if (rewritten.host != originalHost) { request.headers.remove(HttpHeaders.Authorization) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriter.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriter.kt index e0cf94d96..65436d750 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriter.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/MirrorRewriter.kt @@ -31,17 +31,6 @@ object MirrorRewriter { } }.getOrNull() - /** - * Two template styles supported: - * - * - `{url}` (whole-URL proxy): substituted with the full GitHub URL. - * Used by ghfast.top, gh-proxy.com, etc. - * - * - `{owner}/{repo}@{ref}/{path}` (path-decomposed): used by jsDelivr's - * `/gh/` endpoint and similar CDN-style mirrors. Parses the source - * GitHub URL to extract the four placeholders. Returns null if the - * source URL doesn't decompose cleanly. - */ fun applyTemplate( template: String, githubUrl: String, @@ -77,7 +66,6 @@ object MirrorRewriter { val segments = parsed.encodedPath.trimStart('/').split('/').filter { it.isNotEmpty() } return when (host) { "raw.githubusercontent.com" -> { - // /{owner}/{repo}/{ref}/{path...} if (segments.size < 4) return null Decomposed( owner = segments[0], @@ -87,7 +75,6 @@ object MirrorRewriter { ) } "github.com" -> { - // /{owner}/{repo}/raw/{ref}/{path...} or /{owner}/{repo}/blob/{ref}/{path...} if (segments.size < 5) return null val verb = segments[2] if (verb != "raw" && verb != "blob") return null diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt index eab4d0f7f..26eee5310 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt @@ -38,18 +38,8 @@ object ProxyManager { flows.getValue(scope).value = config } - /** - * Resolved active mirror — template plus the traffic kinds it can serve. - * Read on every outbound GitHub request; must be hot-path safe (atomic, no I/O). - * Null when preference is Direct or the catalog has no template for the - * selected mirror. - */ fun currentMirror(): MirrorActive? = mirror.get() - /** - * Convenience accessor for callers that only need the template string and - * don't gate by traffic kind. Prefer [currentMirror] for new code. - */ fun currentMirrorTemplate(): String? = mirror.get()?.template fun startMirrorCollector( diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt index 6419f6825..e2793c55d 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt @@ -1,11 +1,3 @@ package zed.rainxch.core.data.network -/** - * Marker type that represents "the [ProxyManager] has been seeded - * with the user's persisted per-scope proxy configs." Any component - * that constructs an HTTP client whose proxy is read from - * [ProxyManager] at creation time must inject this so Koin resolves - * the seeding step first — it forces the DI graph dependency to be - * explicit instead of depending on registration order. - */ class ProxyManagerSeeding internal constructor() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt index bfbbba42a..949f0ac1a 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt @@ -14,15 +14,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import zed.rainxch.core.domain.model.ProxyConfig -/** - * Reactive holder for the HTTP client used by translation requests. - * Rebuilds the underlying client whenever the translation-scope - * proxy config changes so README translation picks up proxy updates - * without requiring an app restart. Mirrors [GitHubClientProvider] - * but keeps translation on its own client — translation endpoints - * (e.g. Google Translate) don't need any of the GitHub-specific - * interceptors, auth headers, or base URL defaults. - */ class TranslationClientProvider( proxyConfigFlow: StateFlow, ) { @@ -38,9 +29,7 @@ class TranslationClientProvider( .distinctUntilChanged() .onEach { config -> mutex.withLock { - // Build the replacement *before* closing the old one - // so the volatile read from [client] never observes a - // closed-but-not-yet-reassigned client. + val replacement = createPlatformHttpClient(config) val previous = currentClient currentClient = replacement diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/HostTokenInterceptor.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/HostTokenInterceptor.kt deleted file mode 100644 index 3e0bfd558..000000000 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/interceptor/HostTokenInterceptor.kt +++ /dev/null @@ -1,36 +0,0 @@ -package zed.rainxch.core.data.network.interceptor - -import io.ktor.client.plugins.api.createClientPlugin -import io.ktor.client.request.header -import io.ktor.http.HttpHeaders -import kotlin.coroutines.cancellation.CancellationException -import zed.rainxch.core.domain.model.HostNames -import zed.rainxch.core.domain.repository.HostTokenRepository - -class HostTokenInterceptorConfig { - lateinit var repository: HostTokenRepository -} - -val HostTokenInterceptor = createClientPlugin("HostTokenInterceptor", ::HostTokenInterceptorConfig) { - val repo = pluginConfig.repository - onRequest { request, _ -> - val existing = request.headers[HttpHeaders.Authorization] - if (!existing.isNullOrBlank()) return@onRequest - // `api.github.com` requests must map back to the `github.com` - // storage key, otherwise the PAT is never attached. Forgejo / - // Codeberg / Gitea APIs share the host with storage, so the - // mapping is a no-op for them. - val host = HostNames.apiHostToTokenHost(request.url.host) - if (host.isBlank()) return@onRequest - val token = try { - repo.get(host)?.token - } catch (ce: CancellationException) { - throw ce - } catch (_: Throwable) { - null - } - if (!token.isNullOrBlank()) { - request.header(HttpHeaders.Authorization, "Bearer $token") - } - } -} diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsCacheStoreImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsCacheStoreImpl.kt index eef9d7b77..959a0147e 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsCacheStoreImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsCacheStoreImpl.kt @@ -68,7 +68,7 @@ class AnnouncementsCacheStoreImpl( if (!legacy.isNullOrEmpty()) { val putOk = ksafe.safePut(K_CACHED_PAYLOAD, legacy) if (!putOk) { - // Don't drop the only copy if write failed; retry next launch. + return } runCatching { diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt index 1b9611afe..d8e1d23b0 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ExternalImportRepositoryImpl.kt @@ -24,9 +24,8 @@ import zed.rainxch.core.data.local.db.entities.SigningFingerprintEntity import zed.rainxch.core.data.mappers.toRepoMatchResults import zed.rainxch.core.data.mappers.toRequestItem import zed.rainxch.core.data.network.BackendApiClient -import zed.rainxch.core.data.network.BackendException import zed.rainxch.core.data.network.ExternalMatchApi -import zed.rainxch.core.data.network.RateLimitedException +import zed.rainxch.core.data.network.ForgejoClientRegistry import zed.rainxch.core.domain.repository.ExternalImportRepository import zed.rainxch.core.domain.system.ExternalAppCandidate import zed.rainxch.core.domain.system.ExternalAppScanner @@ -38,6 +37,8 @@ import zed.rainxch.core.domain.system.RepoMatchSuggestion import zed.rainxch.core.domain.system.ScanResult import zed.rainxch.core.data.secure.safeGet import zed.rainxch.core.data.secure.safePut +import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.system.InstallerKind class ExternalImportRepositoryImpl( private val scanner: ExternalAppScanner, @@ -47,12 +48,11 @@ class ExternalImportRepositoryImpl( private val legacyDataStore: DataStore, private val externalMatchApi: ExternalMatchApi, private val backendClient: BackendApiClient, - private val forgejoClientRegistry: zed.rainxch.core.data.network.ForgejoClientRegistry, - private val tweaksRepository: zed.rainxch.core.domain.repository.TweaksRepository, + private val forgejoClientRegistry: ForgejoClientRegistry, + private val tweaksRepository: TweaksRepository, ) : ExternalImportRepository { private val candidateSnapshot = MutableStateFlow>(emptyMap()) - override fun pendingCandidatesFlow(): Flow> = combine( candidateSnapshot, @@ -133,7 +133,6 @@ class ExternalImportRepositoryImpl( val existing = externalLinkDao.get(pkg) if (rawCandidate == null) { - // Package is genuinely gone (uninstalled). Hard-delete the row. if (existing != null) { externalLinkDao.deleteByPackageName(pkg) } @@ -141,21 +140,14 @@ class ExternalImportRepositoryImpl( } if (!hasPositiveEvidence(rawCandidate)) { - // Package exists but currently has no positive evidence. Only - // drop PENDING_REVIEW rows here — preserved decisions - // (MATCHED / NEVER_ASK / SKIPPED) must survive a transient - // evidence miss (e.g., F-Droid seed not yet synced, manifest - // hint changed across reinstall). Deleting them on a single - // transient miss would silently wipe the user's decisions. if (existing?.state == ExternalLinkState.PENDING_REVIEW.name) { externalLinkDao.deleteByPackageName(pkg) } return@forEach } - val candidate = rawCandidate - deltaCandidates += candidate - val updated = mergeCandidate(existing, candidate, now) + deltaCandidates += rawCandidate + val updated = mergeCandidate(existing, rawCandidate, now) if (existing == null) newCandidates++ if (updated.state == ExternalLinkState.PENDING_REVIEW.name) pendingReview++ externalLinkDao.upsert(updated) @@ -182,9 +174,6 @@ class ExternalImportRepositoryImpl( override suspend fun resolveMatches(candidates: List): List { if (candidates.isEmpty()) return emptyList() - // Strategy 3: signing-fingerprint lookup against the local seed - // table. Hits are the strongest non-manifest signal we have — - // signature equality is cryptographic, no string fuzzing. val fingerprintHits = mutableMapOf() candidates.forEach { candidate -> val fp = candidate.signingFingerprint ?: return@forEach @@ -219,36 +208,13 @@ class ExternalImportRepositoryImpl( } } - // Strategy 4: query each configured Forgejo / Codeberg host using - // the app label as a free-text search term. Strict bounds: - // - // * Only run for candidates with no high-confidence existing - // hit (manifest / fingerprint / backend ≥ 0.7). Most installed - // apps either match by fingerprint or have nothing useful to - // match against; running a 5-host fanout per candidate - // blew up scan time from seconds to minutes for users with - // larger installed-app lists. - // * Cap the total number of candidates that trigger a Forgejo - // search per scan invocation, so even worst-case unmatched - // populations don't burn through Codeberg's rate limit - // (2000 req / 300s per IP). - // * The pricier `resolveMatches` callers (full scan) are - // already off the main thread; smart-match's single- - // candidate variant skips the cap. val forgejoHits = mutableMapOf>() val forgejoHostList = forgejoSearchHosts() - // When the user has explicitly added custom forges, they care - // about forge results enough to warrant a wider sweep — double - // the budget so more apps actually get cross-checked against - // the user's hosts before we cap out. val hasUserHosts = runCatching { tweaksRepository.getCustomForgeHosts().first().isNotEmpty() }.getOrDefault(false) if (forgejoHostList.isNotEmpty()) { val totalBudget = if (hasUserHosts) FORGEJO_SEARCH_CANDIDATE_BUDGET * 2 else FORGEJO_SEARCH_CANDIDATE_BUDGET - // Build the list of (candidate, query) pairs eligible for - // Forgejo search, applying the skip-threshold filter and - // candidate budget *before* we issue any HTTP. val eligible = candidates.asSequence() .filter { candidate -> val existing = listOfNotNull( @@ -265,12 +231,6 @@ class ExternalImportRepositoryImpl( .take(totalBudget) .toList() - // Cross-product (eligibleCandidate × host) flattened into a - // single list, then fanned out in parallel using a bounded - // semaphore so we don't open more than N concurrent HTTP - // sockets at once. Per-call timeout caps tail latency from - // a slow host so a single dead instance can't drag the - // whole scan past the user's patience threshold. data class SearchTask( val packageName: String, val host: String, @@ -319,9 +279,6 @@ class ExternalImportRepositoryImpl( backendResults[candidate.packageName]?.let { suggestions += it } forgejoHits[candidate.packageName]?.let { suggestions += it } val deduped = suggestions - // Dedup across hosts too — a repo can legitimately exist - // on both Codeberg and a mirror, but for suggestions we - // only want one row per logical {host, owner, repo}. .distinctBy { "${it.sourceHost ?: "github"}|${it.owner}/${it.repo}" } .sortedByDescending { it.confidence } @@ -461,9 +418,6 @@ class ExternalImportRepositoryImpl( RepoMatchSuggestion( owner = item.owner.login, repo = item.name, - // Search is the user-driven override path. The - // 0.5 confidence is a placeholder — UX is "I'll - // pick this myself", not a confidence bet. confidence = SEARCH_OVERRIDE_CONFIDENCE, source = RepoMatchSource.SEARCH, stars = item.stargazersCount, @@ -474,7 +428,6 @@ class ExternalImportRepositoryImpl( } override suspend fun syncSigningFingerprintSeed() { - val started = nowMillis() var rowsAdded = 0 try { val lastObservedAt = runCatching { signingFingerprintDao.lastSyncTimestamp() } @@ -556,7 +509,6 @@ class ExternalImportRepositoryImpl( if (existing != null && shouldPreserveDecision(existing, now)) { return existing.copy( signingFingerprint = candidate.signingFingerprint ?: existing.signingFingerprint, - // installerKind is authoritative per-scan from PackageManager; signingFingerprint may briefly be null on extraction failure, so we hold the previous value. installerKind = candidate.installerKind.name, ) } @@ -590,69 +542,7 @@ class ExternalImportRepositoryImpl( private fun nowMillis(): Long = Clock.System.now().toEpochMilliseconds() - private fun bucketCount(n: Int): String = - when { - n <= 0 -> "0" - n <= 2 -> "1-2" - n <= 9 -> "3-9" - n <= 49 -> "10-49" - else -> "50+" - } - - private fun bucketCandidateCount(n: Int): String = - when { - n <= 0 -> "0" - n <= 9 -> "1-9" - n <= 49 -> "10-49" - n <= 199 -> "50-199" - else -> "200+" - } - - private fun bucketDurationMs(ms: Long): String = - when { - ms < 500L -> "<500" - ms < 2_000L -> "500-2000" - ms < 5_000L -> "2000-5000" - else -> ">5000" - } - - private fun bucketConfidence(c: Double): String = - when { - c < 0.5 -> "<0.5" - c < 0.85 -> "0.5-0.85" - else -> ">=0.85" - } - - private fun bucketSeedRowsAdded(n: Int): String = - when { - n <= 0 -> "0" - n <= 99 -> "1-99" - n <= 999 -> "100-999" - else -> "1000+" - } - - private fun bucketApiFailure(error: Throwable): String = - when (error) { - is BackendException -> { - val code = error.statusCode - if (code in 400..499) "4xx" else "5xx" - } - is RateLimitedException -> "4xx" - else -> "network" - } - - /** - * Canonical Forgejo hosts we always probe (Codeberg + the public - * Gitea/Forgejo demo instances) merged with whatever the user added - * under Tweaks → Network → Custom forges. We cap the merged set at - * 5 hosts to bound concurrent fanout per match attempt — extra - * hosts are dropped silently rather than queued. - */ private suspend fun forgejoSearchHosts(): List { - // Mirror the known-Forgejo set in RepositoryUrlParser so the - // URL-parse path and the smart-match path stay aligned — a URL - // we accept as a Forgejo link is also a host we'll proactively - // search during auto-import. val canonical = listOf("codeberg.org", "gitea.com", "git.disroot.org") val user = runCatching { tweaksRepository.getCustomForgeHosts().first() } .getOrNull() @@ -672,14 +562,8 @@ class ExternalImportRepositoryImpl( limit = FORGEJO_SEARCH_LIMIT, ).getOrNull() ?: return emptyList() response.data - .orEmpty() .take(FORGEJO_SEARCH_LIMIT) .mapIndexed { index, repo -> - // Confidence falls off with rank so the top hit on - // a given host outranks lower hits — but stays - // below the high-confidence GitHub strategies so - // manifest / fingerprint / backend matches still - // sort above when present. val confidence = FORGEJO_SEARCH_BASE_CONFIDENCE - (index * FORGEJO_SEARCH_RANK_DECAY) RepoMatchSuggestion( @@ -704,42 +588,17 @@ class ExternalImportRepositoryImpl( private const val K_INITIAL_SCAN_AT = "external_import_initial_scan_at" private const val SKIP_TTL_MILLIS: Long = 7L * 24 * 60 * 60 * 1000 - // Bounds the per-match fanout so a user with many custom forges - // doesn't trigger a many-way HTTP burst on every smart-match. private const val FORGEJO_SEARCH_MAX_HOSTS = 5 private const val FORGEJO_SEARCH_LIMIT = 5 - // Hard cap on how many candidates trigger a Forgejo fanout per - // resolveMatches call. Smart-match always sends a single - // candidate so this only matters for the import-scan path with - // dozens to hundreds of installed apps. 12 unmatched candidates - // × 5 hosts = at most 60 HTTP calls per scan — well under - // Codeberg's rate limit and finishes in seconds. private const val FORGEJO_SEARCH_CANDIDATE_BUDGET = 12 - // Confidence threshold above which we DON'T bother running the - // Forgejo search — the GitHub-side hit will dominate the - // suggestion list and a remote search would be pure waste. 0.7 - // matches the backend's "confident" bucket. private const val FORGEJO_SEARCH_SKIP_THRESHOLD = 0.7 - // Concurrency cap for the parallel fanout — each permit holds - // one open HTTP socket. 8 keeps us well below mobile network - // stacks' typical 10–16 socket cap and the per-host Forgejo - // rate limit (2000 / 300s) at 26 req/s burst headroom even if - // every permit hits the same host (which they won't, since - // tasks alternate hosts in interleaved order). private const val FORGEJO_SEARCH_CONCURRENCY = 8 - // Hard per-call timeout. The shared HttpClient still has a 60s - // request timeout for the install path; this is a tighter - // wrapper just for smart-match so a single slow / dead host - // doesn't drag the scan latency to the floor. private const val FORGEJO_SEARCH_PER_CALL_TIMEOUT_MS = 4_000L - // Sits below the high-confidence GitHub strategies (manifest / - // fingerprint / backend) but above zero so Forgejo hits still - // surface when nothing else matched. private const val FORGEJO_SEARCH_BASE_CONFIDENCE = 0.55 private const val FORGEJO_SEARCH_RANK_DECAY = 0.08 private const val MATCH_BATCH_SIZE = 25 @@ -750,12 +609,10 @@ class ExternalImportRepositoryImpl( private const val MAX_SEED_PAGES = 50 private const val SEED_SOURCE_BACKEND = "backend_seed" - // Stores whose entire catalog is sourced from GitHub releases — apps installed - // through them are surfaced even without a manifest hint or fingerprint match. private val TRUSTED_GITHUB_INSTALLERS = setOf( - zed.rainxch.core.domain.system.InstallerKind.STORE_OBTAINIUM, - zed.rainxch.core.domain.system.InstallerKind.STORE_FDROID, + InstallerKind.STORE_OBTAINIUM, + InstallerKind.STORE_FDROID, ) } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt index 5803cf126..47fa8e6fd 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt @@ -43,15 +43,10 @@ class HostTokenRepositoryImpl( private val json = Json { ignoreUnknownKeys = true } init { - // Kick off the initial KSafe read so the first collector of - // `observeAll()` already sees the persisted list rather than an - // empty value emitted from a fresh `MutableStateFlow`. + initScope.launch { runCatching { ensureLoaded() } } } - // `onStart` guarantees the load runs before the first emission, so - // collectors that subscribe before `init { … }` lands still get the - // persisted list as their initial value. override fun observeAll(): Flow> = cache.asStateFlow().onStart { ensureLoaded() } @@ -148,10 +143,6 @@ class HostTokenRepositoryImpl( loaded = true } - // Persists then promotes the in-memory cache. If KSafe write fails, - // the in-memory value is rolled back and the exception is thrown so - // the caller can surface the failure to the UI instead of pretending - // the save succeeded. private suspend fun persistOrThrow(list: List) { val previous = cache.value val encoded = json.encodeToString(ListSerializer(HostToken.serializer()), list) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt index 1f3b20a1b..836976515 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt @@ -42,25 +42,11 @@ class InstalledAppsRepositoryImpl( private val backendApiClient: zed.rainxch.core.data.network.BackendApiClient, private val forgejoClientRegistry: zed.rainxch.core.data.network.ForgejoClientRegistry, ) : InstalledAppsRepository { - // Reads the current Ktor client at every call site so any proxy - // change (ProxyManager rebuilds the client via [clientProvider]) - // is picked up immediately on the next request without requiring - // the repository itself to be reconstructed. + private val httpClient: HttpClient get() = clientProvider.client private companion object { - /** - * How many releases the update checker fetches in one request. - * Picked to balance: - * - Monorepos that ship multiple sibling apps in close succession - * (need a few releases of headroom to find a match for the - * targeted app via [InstalledApp.fallbackToOlderReleases]) - * - Avoiding unnecessary GitHub API quota burn for the common case - * of a single-app repo where 1 release is enough. - * - * 50 is the GitHub API per_page maximum that doesn't require - * pagination, and is enough to cover ~3 months of daily releases. - */ + const val RELEASE_WINDOW = 50 } @@ -109,20 +95,6 @@ class InstalledAppsRepositoryImpl( installedAppsDao.deleteByPackageName(packageName) } - /** - * Fetches up to [RELEASE_WINDOW] releases for [owner]/[repo], filters - * out drafts, applies the pre-release policy, and returns them sorted - * by `publishedAt` descending. Empty list on failure (logged at error). - * - * Pre-release policy: a release is filtered out when - * `includePreReleases = false` AND either the GitHub `prerelease` - * flag is `true` OR the tag/name contains a recognised pre-release - * marker (see [GithubRelease.isEffectivelyPreRelease]). The tag - * heuristic catches the common maintainer mistake of tagging - * `v2.0.0-rc.1` with `prerelease: false`. Whenever the flag and - * heuristic disagree we emit a diagnostic so the drift is - * traceable in session logs. - */ private suspend fun fetchReleaseWindow( owner: String, repo: String, @@ -184,8 +156,7 @@ class InstalledAppsRepositoryImpl( .filter { includePreReleases || !it.isEffectivelyPreRelease() } .toList() } catch (e: CancellationException) { - // Structured concurrency: cancellation must propagate. Never - // silently convert a cancelled fetch into an empty result. + throw e } catch (e: Exception) { Logger.e { "Failed to fetch releases for $owner/$repo: ${e.message}" } @@ -193,51 +164,12 @@ class InstalledAppsRepositoryImpl( } } - /** - * Result of [resolveTrackedRelease] — a candidate release plus the asset - * the installer should download for it. `null` when no release in the - * window contains a usable asset (after filter + arch matching). - * - * [variantWasLost] is true when the user has a [InstalledApp.preferredAssetVariant] - * set but none of this release's assets matched it. The caller flips - * `preferredVariantStale` based on this so the UI can prompt the user - * to pick a new variant. - */ private data class ResolvedRelease( val release: GithubRelease, val primaryAsset: GithubAsset, val variantWasLost: Boolean, ) - /** - * Walks [releases] (already in newest-first order) and returns the first - * release whose installable asset list — after applying [filter] — yields - * a usable asset. The picker tries, in order: - * - * 1. **Token-set match** — pinned token fingerprint equals the asset's - * 2. **Glob match** — pinned glob pattern equals the asset's - * 3. **Tail-string match** — legacy substring-tail equality - * 4. **Same-position fallback** — same index, same total count of - * installable assets as when the user originally pinned - * 5. **Platform auto-pick** — architecture-aware default - * - * Layers 1–3 are wrapped behind [AssetVariant.resolvePreferredAsset]. - * Layer 4 is consulted only when 1–3 all miss but the new release - * has exactly the same number of installable assets as the picked - * release. Layer 5 keeps updates flowing even when the variant is - * completely lost — the caller flips `variantWasLost` so the UI - * can surface the discrepancy. - * - * When [filter] is null, only the first release in the window is - * considered: this preserves the pre-existing behaviour for apps that - * don't track a monorepo. - * - * When [filter] is non-null and [fallbackToOlderReleases] is false, the - * walker still only inspects the first release. The semantics are: - * "Apply the filter to the latest release, but don't dig further." - * This matches Obtainium's defaults and avoids accidental downgrades for - * apps where the user just wants a stricter asset picker. - */ private fun resolveTrackedRelease( releases: List, filter: AssetFilter?, @@ -259,10 +191,6 @@ class InstalledAppsRepositoryImpl( releases.take(1) } - // "Has any pin" tracks whether the user has *something* stored - // for variant identity — used to decide whether the - // `variantWasLost` flag should flip on. Without this, an app - // that's never been pinned would always look "lost". val hasAnyPin = preferredVariant != null || preferredTokens.isNotEmpty() || @@ -277,7 +205,6 @@ class InstalledAppsRepositoryImpl( if (installableForApp.isEmpty()) continue - // Layers 1–3: token set, glob, then legacy tail string. val fingerprintMatch = AssetVariant.resolvePreferredAsset( assets = installableForApp, @@ -286,9 +213,6 @@ class InstalledAppsRepositoryImpl( pinnedGlob = preferredGlob, ) - // Layer 4: same-position fallback. Only consulted when no - // fingerprint matched and the user actually pinned - // *something* (otherwise the index is meaningless). val positionMatch = if (fingerprintMatch == null && hasAnyPin) { AssetVariant.resolveBySamePosition( @@ -300,26 +224,6 @@ class InstalledAppsRepositoryImpl( null } - // Layer 5: platform auto-pick (last resort, never null - // unless the platform installer can't pick anything). - // - // Scope the auto-pick input by package flavor: when the - // tracked app's package id has no flavor token (e.g. plain - // `com.foo.bar`), drop release assets that look like flavor - // variants (`-fdroid.apk`, `-foss.apk`) so the picker - // doesn't silently swap the user's installed APK for a - // sibling artifact that ships under a *different* package - // id (`com.foo.bar.fdroid`). The mirror also applies: a - // tracked `.fdroid` package keeps fdroid-named assets. - // Pinned variants are unaffected — fingerprintMatch / - // positionMatch run against the full installable set above. - // Stem filter (issue #591): when the tracked app has a known - // prior asset name, restrict the auto-pick pool to release - // assets that share its base-name stem. Stops sibling apps - // shipped from the same repo (`AppA-1.10.apk` vs - // `AppB-2.20.apk`) from cross-pollinating: without this, - // `choosePrimaryAsset` would pick the highest numeric - // version regardless of the filename prefix. val installedStem = installedAssetName ?.let { AssetVariant.extractBaseStem(it) } @@ -335,11 +239,7 @@ class InstalledAppsRepositoryImpl( pool.filter { AssetVariant.extractBaseStem(it.name) == installedStem } - // Only honour the stem filter when it - // actually keeps something; otherwise fall - // back to the broader pool. A maintainer - // legitimately renaming the binary should - // not strand the update check. + if (matching.isNotEmpty()) matching else pool } } @@ -348,11 +248,6 @@ class InstalledAppsRepositoryImpl( ?: installer.choosePrimaryAsset(autoPickPool) ?: continue - // The variant is "lost" when the user had a pin but neither - // a fingerprint nor a same-position match recovered it. - // Same-position rescues silently (it's a confidence-trick - // — the user can't tell anything went wrong) so we don't - // flag it as lost; otherwise the UI would nag every check. val variantWasLost = hasAnyPin && fingerprintMatch == null && positionMatch == null @@ -379,16 +274,11 @@ class InstalledAppsRepositoryImpl( ) if (releases.isEmpty()) { - // The repo has no visible releases (or the fetch failed - // softly). Drop any stale update metadata so the badge - // doesn't outlive the release that set it. + installedAppsDao.clearUpdateMetadata(packageName, System.currentTimeMillis()) return false } - // Compile the per-app filter once. Invalid regexes are treated as - // "no filter" so we don't break the app silently — the user is - // told about the syntax error in the advanced settings sheet. val compiledFilter = AssetFilter.parse(app.assetFilterRegex) ?.onFailure { error -> @@ -417,21 +307,13 @@ class InstalledAppsRepositoryImpl( "No matching release found for ${app.appName} in window of ${releases.size}; " + "filter=${app.assetFilterRegex}, fallback=${app.fallbackToOlderReleases}" } - // Filter matches nothing in the fetched window — clear - // any cached latest-release metadata so the UI doesn't - // keep pointing at an asset that no longer matches. + installedAppsDao.clearUpdateMetadata(packageName, System.currentTimeMillis()) return false } val (matchedRelease, primaryAsset, variantWasLost) = resolved - // Canary: when installedVersionCode and latestVersionCode are - // both known and equal, the user already has the matched - // release. The tag-string compare below can still flip true - // (#515) if `installedVersion` got out of sync — e.g. after - // a self-update where the resolver path missed updating the - // tag. Trust the version-code parity over the string compare. val installedCode = app.installedVersionCode val latestCode = app.latestVersionCode val codesAlreadyMatch = @@ -440,10 +322,6 @@ class InstalledAppsRepositoryImpl( latestCode > 0L && installedCode == latestCode - // Auto-unskip when a strictly newer release than the one - // the user skipped lands. We bias toward re-notifying so a - // stale skip can't pin the badge off forever — users can - // re-skip the new tag if they don't want it either. val skippedTag = app.skippedReleaseTag val matchesSkipped = skippedTag != null && @@ -490,12 +368,6 @@ class InstalledAppsRepositoryImpl( latestReleasePublishedAt = matchedRelease.publishedAt, ) - // Backfill: when codes already match but the row's - // `installedVersion` (tag) drifted from the matched release - // (typical for rows written before #515 was fixed — e.g. - // installedVersion="1.7.0" left over from before a - // self-update completed), pin the tag to the matched - // release so subsequent checks don't keep re-flagging. if (codesAlreadyMatch && app.installedVersion != matchedRelease.tagName ) { @@ -508,10 +380,6 @@ class InstalledAppsRepositoryImpl( ) } - // Sync the staleness flag with what the resolver actually - // observed: flip on when the user's pinned variant has - // disappeared from the latest matching release, flip off - // (and only when previously set) when it's back in business. if (variantWasLost != app.preferredVariantStale) { installedAppsDao.updateVariantStaleness(packageName, variantWasLost) } @@ -568,14 +436,6 @@ class InstalledAppsRepositoryImpl( ), ) - // Recompute `isUpdateAvailable` against the EXISTING upstream - // `latestVersion` snapshot, not the freshly installed tag. When - // the user explicitly picks an older release (#542 reporter - // scenario) `newTag` < `latestVersion`, so the row still has - // an upstream update pending and the badge must NOT be cleared. - // The previous write stamped `latest* = new*` which then poisoned - // the `checkForUpdates` versionCode-parity canary into reporting - // "up to date" forever. val snapshotLatestVersion = app.latestVersion val isUpdateStillAvailable = !snapshotLatestVersion.isNullOrBlank() && @@ -593,13 +453,7 @@ class InstalledAppsRepositoryImpl( lastUpdatedAt = System.currentTimeMillis(), lastCheckedAt = System.currentTimeMillis(), signingFingerprint = signingFingerprint, - // When this update settled (isPendingInstall = false), - // wipe any leftover parked-install pointer so the apps - // screen stops advertising an Install CTA on a file - // the system already swapped under the row. Caller - // (DetailsViewModel.saveInstalledAppToDatabase) re-sets - // these explicitly via setPendingInstallFilePath - // afterwards on the still-pending path. + pendingInstallFilePath = if (isPendingInstall) app.pendingInstallFilePath else null, pendingInstallVersion = @@ -677,9 +531,6 @@ class InstalledAppsRepositoryImpl( fallback = fallbackToOlderReleases, ) - // Persisting is the authoritative operation — if the follow-up - // re-check fails (network down, rate limited, cancelled) we still - // keep the new filter. The next periodic worker run will catch up. try { checkForUpdates(packageName) } catch (e: CancellationException) { @@ -712,9 +563,6 @@ class InstalledAppsRepositoryImpl( siblingCount = siblingCount?.takeIf { it > 0 }, ) - // Re-run the update check so cached `latestAsset*` columns point - // at the variant the user just chose. Failures here are - // non-fatal: persistence is the authoritative step. try { checkForUpdates(packageName) } catch (e: CancellationException) { @@ -810,11 +658,6 @@ class InstalledAppsRepositoryImpl( ) } - // Version normalization + comparison lives in - // `core.domain.util.VersionMath` so the periodic update check, - // the external-install verdict in `PackageEventReceiver`, and any - // future surfaces all share one comparator. See #378. - private suspend fun fetchForgejoReleaseWindow( host: String, owner: String, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt index 13b80047d..83343e419 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt @@ -166,7 +166,7 @@ class ProxyRepositoryImpl( } val snapshot = runCatching { legacyDataStore.data.first() }.getOrNull() if (snapshot == null) { - // Don't mark complete — retry on next launch. + return } @@ -193,7 +193,6 @@ class ProxyRepositoryImpl( val user = snapshot[userLegacyKey] ?: snapshot[stringPreferencesKey("proxy_username")] val pass = snapshot[passLegacyKey] ?: snapshot[stringPreferencesKey("proxy_password")] - // Per-field tracking — only enqueue legacy delete if KSafe write succeeded. val typeOk = ksafe.safePut(keys.type, type) if (!typeOk) { anyFailure = true; return@forEach } scopeType?.let { keysToClear += typeLegacyKey } @@ -216,9 +215,6 @@ class ProxyRepositoryImpl( } } - // Drop the unscoped legacy keys only if we actually transferred them - // into at least one scope above. We don't know which scope owns them, - // but their fallback role is exhausted once any scope is populated. val anyScopeTouched = keysToClear.isNotEmpty() if (anyScopeTouched) { keysToClear += stringPreferencesKey("proxy_type") diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt index 4fdfe3957..4f6734708 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt @@ -44,7 +44,7 @@ class StarredRepositoryImpl( private val httpClient: HttpClient get() = clientProvider.client companion object { - private const val SYNC_THRESHOLD_MS = 24 * 60 * 60 * 1000L // 24 hours + private const val SYNC_THRESHOLD_MS = 24 * 60 * 60 * 1000L } override fun getAllStarred(): Flow> = diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/secure/DataStoreToKSafeMigrator.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/secure/DataStoreToKSafeMigrator.kt index 6fe271987..6fa0abcf0 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/secure/DataStoreToKSafeMigrator.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/secure/DataStoreToKSafeMigrator.kt @@ -6,34 +6,19 @@ import androidx.datastore.preferences.core.edit import eu.anifantakis.lib.ksafe.KSafe import kotlinx.coroutines.flow.first -/** - * One-shot copier: lift values from a legacy [DataStore] into a [KSafe] - * vault. Idempotent — sets a marker in the KSafe vault so subsequent - * launches skip the work. - * - * Each entry is a `(legacyKey, transform)` pair. The transform reads from - * the legacy `Preferences` snapshot and writes into KSafe (decoupling - * primitive vs. derived shapes such as enums or JSON objects). - * - * The legacy keys are removed after a successful copy so the next read - * never sees the duplicate. - */ suspend fun migrateDataStoreToKSafe( legacy: DataStore, ksafe: KSafe, markerKey: String, entries: List, ) { - val alreadyMigrated = runCatching { ksafe.get(markerKey, false) }.getOrDefault(false) + val alreadyMigrated = runCatching { + ksafe.get(markerKey, false) + }.getOrDefault(false) if (alreadyMigrated) return val snapshotResult = runCatching { legacy.data.first() } - val snapshot = snapshotResult.getOrNull() - if (snapshot == null) { - // Legacy file unreadable. Don't set the marker — retry next launch - // so a transient I/O failure doesn't permanently skip migration. - return - } + val snapshot = snapshotResult.getOrNull() ?: return val movedKeys = mutableListOf>() var anyFailure = false @@ -42,12 +27,9 @@ suspend fun migrateDataStoreToKSafe( when { copyResult.isFailure -> anyFailure = true copyResult.getOrNull() == true -> movedKeys += entry.legacyKeys - // null/false → key absent or unsupported type; not a failure } } - // Only delete legacy keys we know we copied successfully. Even on - // partial failure, what we *did* copy is safe to remove. if (movedKeys.isNotEmpty()) { val deleteResult = runCatching { legacy.edit { prefs -> movedKeys.forEach { prefs.remove(it) } } @@ -55,8 +37,6 @@ suspend fun migrateDataStoreToKSafe( if (deleteResult.isFailure) anyFailure = true } - // Only mark complete if every entry succeeded AND legacy cleanup - // succeeded. Otherwise next launch retries the missing entries. if (!anyFailure) { runCatching { ksafe.put(markerKey, true) } } @@ -75,8 +55,7 @@ class MigrationEntry( ): MigrationEntry = MigrationEntry( legacyKeys = listOf(legacyKey), copyBlock = { snapshot, ksafe -> - val value: Any? = snapshot[legacyKey] - if (value == null) return@MigrationEntry false + val value: Any = snapshot[legacyKey] ?: return@MigrationEntry false when (value) { is Boolean -> ksafe.put(ksafeKey, value) is Int -> ksafe.put(ksafeKey, value) @@ -89,6 +68,7 @@ class MigrationEntry( val asStrings = (value as Set).toList() ksafe.put>(ksafeKey, asStrings) } + else -> return@MigrationEntry false } true diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt index a6ced167b..53f9644e9 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultDownloadOrchestrator.kt @@ -31,48 +31,6 @@ import zed.rainxch.core.domain.system.SystemInstallSerializer import zed.rainxch.core.domain.util.AssetFileName import kotlin.random.Random -/** - * Default implementation of [DownloadOrchestrator]. - * - * # Lifetime - * - * Owned by Koin as a singleton with the application-scoped - * [CoroutineScope] (see `coreModule`'s `CoroutineScope` factory). All - * downloads run in [appScope] so they outlive any ViewModel that - * triggered them. The orchestrator itself never cancels its scope — - * the process going away does that for free. - * - * # Concurrency - * - * Uses a per-package [Mutex] surfaced via [stateMutex] to serialize - * map mutations. Actual download/install work runs in [appScope] so - * the mutex is only held for short metadata writes — never across - * the long-running download. - * - * Up to [DEFAULT_MAX_CONCURRENT] downloads run at once. Further - * enqueue calls don't block — they just spawn a coroutine that - * `delay`s while the active count is at capacity. The simpler "always - * spawn, let the platform Downloader queue internally" approach is - * tempting, but OkHttp doesn't queue — it'd open every connection at - * once and likely hit GitHub's rate limit. - * - * # Filename namespacing - * - * Every download is written to disk with the - * `__.ext` form via [AssetFileName.scoped]. - * This is the only place that name transformation lives — callers - * pass the original asset name and the orchestrator handles the rest. - * The scoped name is also what gets passed to [Downloader.download], - * so the Downloader's per-name dedup map is automatically scoped. - * - * # Install policies - * - * See [InstallPolicy] for the rules. Implementation detail: the - * "screen still attached" check at install time is done by re-reading - * `installPolicy` from the live state map *after* the download - * completes — never from a captured local — so a [downgradeToDeferred] - * call lands atomically against the in-flight download. - */ class DefaultDownloadOrchestrator( private val downloader: Downloader, private val multiSourceDownloader: MultiSourceDownloader, @@ -92,18 +50,8 @@ class DefaultDownloadOrchestrator( private val _downloads = MutableStateFlow>(emptyMap()) override val downloads: StateFlow> = _downloads.asStateFlow() - /** - * Mutex guarding map mutations *and* the active-count check used - * by the queue. Held only for short critical sections — never - * across `Downloader.download(...).collect`. - */ private val stateMutex = Mutex() - /** - * Job handles per package, used by [cancel] to interrupt the - * download. Separate from `_downloads` because Job is mutable - * state that doesn't belong in an immutable StateFlow value. - */ private val activeJobs = mutableMapOf() override fun observe(packageName: String): Flow = @@ -112,19 +60,11 @@ class DefaultDownloadOrchestrator( .distinctUntilChanged() override suspend fun enqueue(spec: DownloadSpec): String { - // Idempotent: if a download for this package is already - // running (or queued, or awaiting install), return its id and - // upgrade the install policy if the new caller wants a - // stronger guarantee. Common case: user taps Update twice in - // a row and we don't want to spawn two downloads. stateMutex.withLock { val existing = _downloads.value[spec.packageName] if (existing != null && existing.stage != DownloadStage.Failed && existing.stage != DownloadStage.Cancelled ) { - // Allow caller to upgrade policy: e.g. apps list - // started a Deferred download, then user opened - // Details and we want to flip to InstallWhileForeground. if (existing.installPolicy != spec.installPolicy && existing.installPolicy.priority < spec.installPolicy.priority ) { @@ -162,8 +102,6 @@ class DefaultDownloadOrchestrator( try { runDownload(spec) } catch (e: CancellationException) { - // Honour structured concurrency. The cancel path - // is responsible for cleaning state. Logger.d { "Orchestrator: download cancelled for ${spec.packageName}" } throw e } catch (t: Throwable) { @@ -185,23 +123,7 @@ class DefaultDownloadOrchestrator( return id } - /** - * The actual download/install pipeline. Runs entirely inside - * [appScope]. - * - * 1. Wait for a slot if [DEFAULT_MAX_CONCURRENT] is at capacity - * 2. Stream bytes via [Downloader] under the scoped filename, - * emitting progress updates - * 3. Read the latest [InstallPolicy] from state — this is the - * race-safe read that lets [downgradeToDeferred] land - * 4. Branch on policy: install in-process, defer to user, or - * hand off to the dialog-driven Details flow - */ private suspend fun runDownload(spec: DownloadSpec) { - // Wait for a worker slot. Polling is acceptable here because - // the wait is not on the user's critical path (a queued - // download is by definition something the user is OK waiting - // on). while (true) { val activeNow = stateMutex.withLock { @@ -218,10 +140,6 @@ class DefaultDownloadOrchestrator( originalName = spec.asset.name, ) - // Move to Downloading. Initialize totalBytes from the asset's - // declared size — the server's Content-Length might disagree - // (or be missing), in which case the download flow will - // override it with the live value below. updateEntry(spec.packageName) { it.copy( stage = DownloadStage.Downloading, @@ -236,8 +154,6 @@ class DefaultDownloadOrchestrator( it.copy( progressPercent = progress.percent, bytesDownloaded = progress.bytesDownloaded, - // Prefer the live Content-Length when present; - // fall back to whatever we already had (asset.size). totalBytes = progress.totalBytes ?: it.totalBytes, ) } @@ -251,10 +167,6 @@ class DefaultDownloadOrchestrator( it.copy(filePath = filePath, progressPercent = 100) } - // SHA-256 gate sits between download-complete and the install / - // park transition: a tampered mirror must not be able to feed - // an APK into the installer. Null digest = GitHub didn't expose - // one (older assets, non-asset endpoints); log and proceed. val expectedDigest = spec.asset.digest if (expectedDigest != null) { val mismatch = digestVerifier.verify(filePath, expectedDigest) @@ -271,9 +183,6 @@ class DefaultDownloadOrchestrator( Logger.i { "No digest for ${spec.asset.name}, skipping SHA-256 verification" } } - // Race-safe read of the *current* install policy. The - // ViewModel may have called downgradeToDeferred while the - // download was in flight; that mutation lands here. val effectivePolicy = stateMutex.withLock { _downloads.value[spec.packageName]?.installPolicy ?: spec.installPolicy @@ -282,40 +191,15 @@ class DefaultDownloadOrchestrator( when (effectivePolicy) { InstallPolicy.AlwaysInstall -> runInstall(spec, filePath) - // InstallWhileForeground: park *without* notifying. The - // foreground VM is expected to be observing and will run - // its own dialog-driven install on the file. The notifier - // only fires when the policy gets downgraded to Deferred - // (either explicitly via downgradeToDeferred, or - // originally), so the user only sees a notification when - // there's nobody watching the download interactively. InstallPolicy.InstallWhileForeground -> parkForUser(spec, filePath, notify = false) InstallPolicy.DeferUntilUserAction -> parkForUser(spec, filePath, notify = true) } } - /** - * Invokes the platform installer for [filePath]. On success the - * orchestrator entry is moved to [DownloadStage.Completed] (the - * consuming ViewModel will dismiss it from the map). - * - * Validation (signing fingerprint, package mismatch, etc.) is - * deliberately NOT done here. The dialog-driven path in - * `DetailsViewModel` handles those because they need user - * interaction. The orchestrator's "always install" path is used - * for Shizuku silent installs where the user has explicitly opted - * out of confirmations. - */ private suspend fun runInstall(spec: DownloadSpec, filePath: String) { updateEntry(spec.packageName) { it.copy(stage = DownloadStage.Installing) } val ext = spec.asset.name.substringAfterLast('.', "").lowercase() - // Tracks whether `installer.install` already returned - // DELEGATED_TO_SYSTEM. The system installer dialog is still up - // in that state — releasing the gate from a downstream catch - // would let the next queued install fire ACTION_VIEW into the - // open dialog and reintroduce the stacking bug. Broadcast or - // timeout owns the release in the delegated case. var delegated = false try { installer.ensurePermissionsOrThrow(ext) @@ -324,9 +208,6 @@ class DefaultDownloadOrchestrator( delegated = outcome == InstallOutcome.DELEGATED_TO_SYSTEM when (outcome) { InstallOutcome.COMPLETED -> { - // Synchronous install (Shizuku / Dhizuku) — broadcast - // arrives quickly, but release the gate now so the next - // queued install isn't blocked by a possibly-late receiver. systemInstallSerializer.markCompleted(spec.packageName) try { installedAppsRepository.setPendingInstallFilePath(spec.packageName, null) @@ -361,17 +242,6 @@ class DefaultDownloadOrchestrator( } } - /** - * "Park" path: download finished but the install policy says - * "don't auto-install". Persists the file path on the InstalledApp - * row so the apps list can show a "Ready to install" row, optionally - * fires the notification, and moves the orchestrator entry to - * [DownloadStage.AwaitingInstall]. - * - * @param notify Whether to poke the notifier. False for the - * InstallWhileForeground case (the foreground VM is watching); - * true for the DeferUntilUserAction case (no foreground watcher). - */ private suspend fun parkForUser( spec: DownloadSpec, filePath: String, @@ -388,10 +258,6 @@ class DefaultDownloadOrchestrator( throw e } catch (e: Exception) { Logger.e(e) { "Orchestrator: failed to persist pending install path" } - // Persistence is best-effort — the orchestrator state - // still has the file path, so the row can render via - // observe() during the same process lifetime. Survives - // process death is what we lose. } if (notify) { pendingInstallNotifier.notifyPending( @@ -406,17 +272,12 @@ class DefaultDownloadOrchestrator( } override suspend fun downgradeToDeferred(packageName: String) { - // Snapshot what we need under the lock and decide whether to - // notify outside it (notifier is third-party code, don't hold - // the mutex across it). val shouldNotify: Boolean val notifySpec: NotifyData? stateMutex.withLock { val existing = _downloads.value[packageName] ?: return when (existing.stage) { DownloadStage.Queued, DownloadStage.Downloading -> { - // In-flight: just flip the policy. parkForUser - // will see it later and notify. _downloads.update { state -> state + ( packageName to existing.copy( @@ -429,12 +290,6 @@ class DefaultDownloadOrchestrator( } DownloadStage.AwaitingInstall -> { - // Race: the orchestrator already parked the file - // (silently, because the policy was - // InstallWhileForeground) but the foreground VM - // is now going away. We need to retroactively - // notify the user so they don't lose track of - // the ready-to-install file. val needsNotify = existing.installPolicy == InstallPolicy.InstallWhileForeground _downloads.update { state -> @@ -463,7 +318,6 @@ class DefaultDownloadOrchestrator( DownloadStage.Cancelled, DownloadStage.Failed, -> { - // Too late or terminal — nothing meaningful to do. shouldNotify = false notifySpec = null } @@ -492,10 +346,6 @@ class DefaultDownloadOrchestrator( val job = stateMutex.withLock { activeJobs.remove(packageName) } job?.cancel() - // Best-effort: delete the partial file via the downloader's - // cancellation hook. The downloader's cancel path is keyed - // on the scoped filename — we can recompute it from the - // entry's owner/repo/asset name. val entry = _downloads.value[packageName] if (entry != null) { val scopedName = @@ -508,9 +358,6 @@ class DefaultDownloadOrchestrator( Logger.w(e) { "Orchestrator: cancelDownload failed for $scopedName" } } - // If the entry was parked (AwaitingInstall), clean up the - // persistent pending-install metadata and notification so - // the apps row doesn't keep showing "ready to install". if (entry.stage == DownloadStage.AwaitingInstall) { try { installedAppsRepository.setPendingInstallFilePath(packageName, null) @@ -535,9 +382,6 @@ class DefaultDownloadOrchestrator( override suspend fun installPending(packageName: String): InstallOutcome? { val entry = _downloads.value[packageName] ?: run { - // The orchestrator's in-memory entry may be gone (e.g. - // process restart). Fall back to the persisted - // pendingInstallFilePath on the InstalledApp row. val app = try { installedAppsRepository.getAppByPackage(packageName) } catch (e: CancellationException) { @@ -562,11 +406,6 @@ class DefaultDownloadOrchestrator( ext: String, ): InstallOutcome? { updateEntry(packageName) { it.copy(stage = DownloadStage.Installing) } - // See [runInstall] — once the install hands off to the system - // installer dialog, the gate must stay locked until broadcast - // or timeout releases it; otherwise a downstream cancellation - // would unlock the gate while the dialog is still up and let - // the next queued install stack ACTION_VIEW intents. var delegated = false return try { installer.ensurePermissionsOrThrow(ext) @@ -585,9 +424,6 @@ class DefaultDownloadOrchestrator( pendingInstallNotifier.clearPending(packageName) updateEntry(packageName) { it.copy(stage = DownloadStage.Completed) } } - // DELEGATED_TO_SYSTEM: the system installer dialog is - // showing. Don't clear pending metadata or mark Completed — - // PackageEventReceiver handles the final state transition. outcome } catch (e: CancellationException) { if (!delegated) systemInstallSerializer.markCompleted(packageName) @@ -601,9 +437,6 @@ class DefaultDownloadOrchestrator( } override fun dismiss(packageName: String) { - // Synchronous dismiss — no Mutex needed for the read-modify-write - // because StateFlow.update is itself atomic. We just don't - // touch activeJobs (cancel does that). _downloads.update { it - packageName } } @@ -629,19 +462,9 @@ class DefaultDownloadOrchestrator( } private fun generateId(): String = - // Cheap unique id without pulling in java.util.UUID — works - // on all KMP targets. Collisions are statistically negligible - // for the lifetime of the orchestrator. Random.nextLong().toULong().toString(36) + Random.nextLong().toULong().toString(36) } -/** - * Orderable strength of an install policy. Higher = stronger - * guarantee. Used by [DefaultDownloadOrchestrator.enqueue] when an - * existing download is re-enqueued with a different policy: we only - * upgrade (never downgrade) so the original caller's intent is - * preserved unless an even more committed caller arrives. - */ private val InstallPolicy.priority: Int get() = when (this) { InstallPolicy.DeferUntilUserAction -> 0 diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultSystemInstallSerializer.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultSystemInstallSerializer.kt index 15cb1b255..9956c7af6 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultSystemInstallSerializer.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/DefaultSystemInstallSerializer.kt @@ -25,7 +25,6 @@ class DefaultSystemInstallSerializer : SystemInstallSerializer { while (!pending.compareAndSet(null, packageName)) { pending.first { it == null } } - Unit } if (acquired == null) { Logger.w { @@ -40,14 +39,6 @@ class DefaultSystemInstallSerializer : SystemInstallSerializer { } override fun markCompleted(packageName: String) { - // Clear unconditionally rather than compareAndSet against the - // marked package: any package install/uninstall completion means - // the system installer activity is no longer holding our prior - // install hostage, so the next gated install can proceed. Using - // strict compareAndSet would leave the slot locked when a broadcast - // fires for a different package than the one we marked (e.g. - // `markPending` was called with an empty string because the APK - // info extractor failed to surface a packageName). pending.value = null } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/LocalizationManager.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/LocalizationManager.kt index 2ff6606eb..3db9ae0fd 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/LocalizationManager.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/services/LocalizationManager.kt @@ -1,27 +1,9 @@ package zed.rainxch.core.data.services interface LocalizationManager { - /** - * Returns the current device language code in ISO 639-1 format (e.g., "en", "zh", "ja") - * Can include region code if available (e.g., "zh-CN", "pt-BR") - */ fun getCurrentLanguageCode(): String - /** - * Returns the primary language code without region (e.g., "zh" from "zh-CN") - */ fun getPrimaryLanguageCode(): String - /** - * Overrides the process-wide JVM `Locale.getDefault()` used by - * Compose Resources' `LocalComposeEnvironment` for string - * resolution. Passing `null` (or blank) restores the original - * system locale captured at instance construction. - * - * Must be called from the composition side (see `App()`) *before* - * the `key(appLanguage)`-wrapped content remounts, so the new - * locale is picked up when `stringResource` re-reads - * `Locale.current` on recomposition. - */ fun setActiveLanguageTag(tag: String?) } diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt index 04f04759d..c0aa960f2 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt @@ -42,7 +42,7 @@ import zed.rainxch.core.domain.utils.ClipboardHelper import zed.rainxch.core.domain.utils.ShareManager actual val corePlatformModule = module { - // Core + single { DesktopDownloader( files = get(), @@ -82,8 +82,6 @@ actual val corePlatformModule = module { DesktopLocalizationManager() } - // Locals - single { initDatabase() } @@ -108,9 +106,6 @@ actual val corePlatformModule = module { eu.anifantakis.lib.ksafe.KSafe(fileName = "ghs_announcements") } - - // Utils - single { DesktopBrowserHelper() } diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt index f2c770225..797f3cce0 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt @@ -7,10 +7,7 @@ import zed.rainxch.core.data.local.DesktopAppDataPaths import java.io.File fun initDatabase(): AppDatabase { - // SQLite WAL mode keeps two side files alongside the .db. Migrate sidecars - // FIRST so the .db never lands at the new location without its WAL — if - // the WAL/SHM copies fail, we abort before touching the .db and let the - // user retry next launch (the legacy files are still in tmp). + DesktopAppDataPaths.migrateFromTmpIfNeeded("github_store.db-wal") DesktopAppDataPaths.migrateFromTmpIfNeeded("github_store.db-shm") DesktopAppDataPaths.migrateFromTmpIfNeeded("github_store.db") diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxPackageType.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxPackageType.kt index b34953c9a..e4d451938 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxPackageType.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/model/LinuxPackageType.kt @@ -1,8 +1,8 @@ package zed.rainxch.core.data.model enum class LinuxPackageType { - DEB, // Debian/Ubuntu/Mint/Pop/Elementary - RPM, // Fedora/RHEL/CentOS/openSUSE/Rocky/Alma - ARCH, // Arch/Manjaro/EndeavourOS/Artix/CachyOS/Garuda - UNIVERSAL, // Unknown - show AppImage only + DEB, + RPM, + ARCH, + UNIVERSAL, } diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt index 942f10aa8..9360d18e3 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt @@ -14,20 +14,11 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient = HttpClient(OkHttp) { engine { config { - // Trust OS-installed root certificates in addition to the - // JVM cacerts bundle. Lets users behind TLS-intercepting - // tools (Watt Toolkit, Fiddler, corporate MITM) keep using - // the app without manually injecting certs into the JDK. - // Silently skipped on platforms where no OS keystore is - // available — default trust still applies. + buildOsTrustChainOrNull()?.let { chain -> sslSocketFactory(chain.socketFactory, chain.trustManager) } - // Reset any inherited global SOCKS authenticator before - // deciding what this client needs — prevents a stale - // Authenticator from a previous [ProxyConfig.Socks] client - // leaking into a subsequently-built plain client. Authenticator.setDefault(null) when (proxyConfig) { @@ -62,14 +53,7 @@ actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient = val proxyHost = proxyConfig.host val proxyPort = proxyConfig.port if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) { - // SOCKS5 username/password auth goes through - // java.net.Authenticator (OkHttp has no - // dedicated SOCKS auth hook). Scope the - // credentials to the configured proxy host - // and port — `Authenticator.setDefault` is - // process-wide, so an unconditional responder - // would leak these creds to any other auth - // challenge the JVM sees. + Authenticator.setDefault( object : Authenticator() { override fun getPasswordAuthentication(): PasswordAuthentication? { diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopApkInspector.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopApkInspector.kt index c44824ec5..973cc518b 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopApkInspector.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopApkInspector.kt @@ -3,11 +3,6 @@ package zed.rainxch.core.data.services import zed.rainxch.core.domain.model.ApkInspection import zed.rainxch.core.domain.system.ApkInspector -/** - * Desktop has no concept of an APK manifest. The inspector is wired in - * for symmetry with Android — every call returns `null` so the UI can - * gate on `inspect(...) != null` without platform branching. - */ class DesktopApkInspector : ApkInspector { override suspend fun inspectFile(filePath: String): ApkInspection? = null diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloadProgressNotifier.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloadProgressNotifier.kt index af304542b..57205cfd0 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloadProgressNotifier.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloadProgressNotifier.kt @@ -2,11 +2,6 @@ package zed.rainxch.core.data.services import zed.rainxch.core.domain.system.DownloadProgressNotifier -/** - * Desktop has no system-shade equivalent that matches the Android - * download-notification UX, so this stays a no-op. The orchestrator - * still calls in unconditionally, avoiding a platform branch. - */ class DesktopDownloadProgressNotifier : DownloadProgressNotifier { override fun notifyProgress( packageName: String, diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt index 84a585f7c..a0a4b702e 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt @@ -82,9 +82,7 @@ class DesktopDownloader( suggestedFileName: String?, bypassMirror: Boolean, ): Flow = - // bypassMirror is a no-op here: this downloader uses OkHttp directly, - // not Ktor, so it never traverses MirrorRewriteInterceptor. The caller - // (MultiSourceDownloader) already passes the resolved direct/mirror URL. + flow { val client = buildClient() @@ -106,8 +104,7 @@ class DesktopDownloader( val downloadId = UUID.randomUUID().toString() val destination = File(dir, safeName) - // Each attempt writes to its own temp file so MultiSourceDownloader's - // direct/mirror race cannot have two jobs trampling the same path. + val tempFile = File(dir, "$safeName.part-$downloadId") if (tempFile.exists()) tempFile.delete() @@ -220,9 +217,7 @@ class DesktopDownloader( override suspend fun cancelDownload(fileName: String): Boolean = withContext(Dispatchers.IO) { - // Cancel every in-flight download for this fileName — MultiSource - // races run direct + mirror in parallel under the same logical - // name, so a single-id lookup would leave one of them running. + val ids = idsByName.remove(fileName)?.toList().orEmpty() if (ids.isEmpty()) return@withContext false diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt index 66b4e7a02..fa7e0c453 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt @@ -32,10 +32,6 @@ class DesktopInstaller( determineSystemArchitecture() } - /** - * Detects whether the app is running inside a Flatpak sandbox. - * Checks for the `/.flatpak-info` file which is always present inside Flatpak containers. - */ private val isRunningInFlatpak: Boolean by lazy { try { File("/.flatpak-info").exists() || @@ -122,10 +118,7 @@ class DesktopInstaller( } Platform.LINUX -> { - // Flatpak sandbox prefers native packages over AppImage - // because AppImages inside Flatpak require extra permission - // dances. Outside Flatpak we prefer AppImage — portable, - // no sudo needed. + if (isRunningInFlatpak) { when (linuxPackageType) { LinuxPackageType.DEB -> listOf(".deb", ".appimage", ".rpm", ".pkg.tar.zst") @@ -292,11 +285,6 @@ class DesktopInstaller( } } - /** - * When running inside a Flatpak sandbox, /etc/os-release belongs to the Flatpak runtime - * (e.g. org.freedesktop.Platform), not the host OS. To detect the host distro we read - * /run/host/os-release, which Flatpak bind-mounts from the host. - */ private fun detectHostLinuxPackageType(): LinuxPackageType { val hostOsRelease = File("/run/host/os-release") if (!hostOsRelease.exists()) { @@ -421,11 +409,7 @@ class DesktopInstaller( return when (platform) { Platform.WINDOWS -> ext in listOf("msi", "exe") Platform.MACOS -> ext in listOf("dmg", "pkg") - // "pkg.tar.zst" keeps the literal double-dotted form the Arch - // convention uses. Dispatch below checks the full filename - // suffix anyway, but callers that only have the ext token - // (e.g. file-extension-only classification paths) would see - // "zst" on its own. Accept "pkg.tar.zst" and bare "zst" both. + Platform.LINUX -> ext in listOf("appimage", "deb", "rpm", "pkg.tar.zst", "zst") else -> false } @@ -500,13 +484,6 @@ class DesktopInstaller( } - /** - * Flatpak-sandboxed installation flow. - * - * Since we can't execute system installers, we use xdg-open (which goes through - * the Flatpak portal to the host) to open the file with the host's default handler. - * This lets the host's software center / file manager handle the actual installation. - */ private fun installFromFlatpak( file: File, ext: String, @@ -514,10 +491,6 @@ class DesktopInstaller( Logger.i { "Running in Flatpak sandbox — delegating installation to host system" } Logger.i { "File: ${file.absolutePath} (.$ext)" } - // Arch packages use the double extension `.pkg.tar.zst`. Callers - // may pass either "pkg.tar.zst" or just "zst" via `ext` depending - // on which classification path computed it, so gate on the - // filename directly — same authoritative check installLinux uses. val nameLower = file.name.lowercase() if (nameLower.endsWith(".pkg.tar.zst")) { Logger.d { "Opening .pkg.tar.zst package via xdg-open portal for host installation" } @@ -609,11 +582,6 @@ class DesktopInstaller( } } - /** - * Show a notification from within the Flatpak sandbox. - * Uses notify-send which goes through the desktop notifications portal. - * Falls back to logging if notifications aren't available. - */ private fun showFlatpakNotification( title: String, message: String, @@ -635,13 +603,6 @@ class DesktopInstaller( } } - /** - * Opens the system file manager with the given file highlighted/selected. - * - * Tries D-Bus FileManager1.ShowItems first (works on GNOME, KDE, etc. and - * goes through the Flatpak portal), then falls back to xdg-open on the - * parent directory. - */ private fun openInFileManager(file: File) { try { val fileUri = "file://${file.absolutePath}" @@ -684,16 +645,7 @@ class DesktopInstaller( } "exe" -> { - // Hand off to ShellExecute via `cmd /c start` rather than - // java.awt.Desktop.open(file). Desktop.open converts the - // file to a URI internally and rejects paths that don't - // round-trip cleanly — non-ASCII characters in the user's - // Windows username (Chinese, Cyrillic, etc.) and unusual - // filename glyphs both surface as "Unsupported URI content" - // (#371). `start` takes the path verbatim through the - // system codepage and works regardless. The empty `""` is - // the window title, required because `start` treats the - // first quoted argument as the title. + try { ProcessBuilder("cmd", "/c", "start", "", file.absolutePath).start() } catch (e: IOException) { @@ -790,11 +742,7 @@ class DesktopInstaller( file: File, ext: String, ) { - // The `ext` parameter is just the final extension token, but - // Arch packages use the double-dotted form `.pkg.tar.zst`. So - // look at the full filename when routing to pacman — a bare - // `.zst` on a non-pacman-shaped filename is genuinely ambiguous - // and we shouldn't hand it to pacman. + val nameLower = file.name.lowercase() if (nameLower.endsWith(".pkg.tar.zst")) { installPacmanPackage(file) @@ -999,41 +947,18 @@ class DesktopInstaller( throw IOException("Could not install RPM package. Please install it manually.") } - /** - * Wraps a string for safe embedding inside a POSIX shell - * single-quoted context. Handles embedded single quotes via the - * `'\''` closing-escaping-reopening trick. - * - * `foo` → `'foo'` - * `foo'bar` → `'foo'\''bar'` - * `don't panic` → `'don'\''t panic'` - * - * Use this whenever a filename (or any externally-sourced string) - * needs to be interpolated into a shell command that ends up being - * executed — terminal command builders, `sh -c` invocations, etc. - */ private fun shellQuoteSingleQuotes(s: String): String = "'" + s.replace("'", "'\\''") + "'" private fun installPacmanPackage(file: File) { Logger.d { "Installing pacman package: ${file.absolutePath}" } - // Wrong-distro case: a `.pkg.tar.zst` on a non-Arch system is - // effectively impossible to install cleanly (no package manager - // knows the format, and conversion tools like `debtap` are - // Arch→Debian not the other way, and require user setup to - // seed their DB first). Show the user a clear terminal message - // instead of silently attempting a path that will fail. if (linuxPackageType != LinuxPackageType.ARCH) { Logger.w { "Pacman package (.pkg.tar.zst) on non-Arch system (type=$linuxPackageType)." } openTerminalForPacmanIncompatible(file.absolutePath) return } - // argv-list invocation — no shell involved, so filenames with - // special chars are passed safely. No `sh -c` fallback needed - // here because pacman -U doesn't need any shell-level chaining - // (unlike DEB's `dpkg || apt-get install -f`). val installMethods = listOf( listOf("pkexec", "pacman", "-U", "--noconfirm", file.absolutePath), @@ -1073,8 +998,7 @@ class DesktopInstaller( val quoted = shellQuoteSingleQuotes(filePath) if (availableTerminals.isEmpty()) { - // Notification body is user-visible text, not a shell command, - // so the raw path is fine to display here. + tryShowNotification( "Install via Terminal", "Run: sudo pacman -U $quoted", @@ -1132,9 +1056,7 @@ class DesktopInstaller( append("echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; ") append("echo ''; ") append("echo 'This file is an Arch Linux package (.pkg.tar.zst):'; ") - // printf treats %s as a plain arg, so the shell-escaped - // $quoted resolves back to the real path when printed — - // no literal escape characters bleed into the display. + append("printf ' %s\\n' $quoted; ") append("echo ''; ") append("echo 'Your system uses a different package format.'; ") @@ -1397,10 +1319,6 @@ class DesktopInstaller( } } - /** - * Move AppImage to ~/Applications directory - * Creates the directory if it doesn't exist - */ private fun moveToApplicationsDirectory(file: File): File { val homeDir = System.getProperty("user.home") val applicationsDir = File(homeDir, "Applications") diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopLocalizationManager.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopLocalizationManager.kt index 83eacefc2..26fb4cb26 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopLocalizationManager.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopLocalizationManager.kt @@ -3,11 +3,7 @@ package zed.rainxch.core.data.services import java.util.Locale class DesktopLocalizationManager : LocalizationManager { - /** - * Snapshot of the original JVM locale at construction time, so - * [setActiveLanguageTag] with a null argument can restore it even - * after prior overrides have modified `Locale.getDefault()`. - */ + private val systemDefault: Locale = Locale.getDefault() override fun getCurrentLanguageCode(): String { diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopPendingInstallNotifier.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopPendingInstallNotifier.kt index ec000cb79..58b4cda66 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopPendingInstallNotifier.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopPendingInstallNotifier.kt @@ -2,16 +2,6 @@ package zed.rainxch.core.data.services import zed.rainxch.core.domain.system.PendingInstallNotifier -/** - * Desktop has no pending-install flow: - * - The apps tab is hidden on Desktop (set via `Platform.ANDROID` gate - * in the bottom nav), so the user has no place to "tap to install" - * - Desktop installs always run in-process and complete synchronously - * via the OS package manager dialog - * - * Notifier is a no-op so the orchestrator can still call into it - * unconditionally without platform branching. - */ class DesktopPendingInstallNotifier : PendingInstallNotifier { override fun notifyPending( packageName: String, diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt index e50a57023..65b575055 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt @@ -2,15 +2,12 @@ package zed.rainxch.core.data.services import zed.rainxch.core.domain.system.UpdateScheduleManager -/** - * No-op implementation for Desktop — WorkManager is Android-only. - */ class DesktopUpdateScheduleManager : UpdateScheduleManager { override fun reschedule(intervalHours: Long) { - // No background scheduler on Desktop + } override fun cancel() { - // No background scheduler on Desktop + } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkInspection.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkInspection.kt index 15e963902..8192b0508 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkInspection.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkInspection.kt @@ -1,89 +1,65 @@ package zed.rainxch.core.domain.model -/** - * Snapshot of an APK's declared metadata. Produced by - * [zed.rainxch.core.domain.system.ApkInspector] from either a file on - * disk (parked install) or an installed package's manifest. - * - * Everything here is *as the APK declares it* — the inspector does not - * resolve it against runtime grant state for permissions, that lives on - * [ApkPermission.granted] which is `null` for file-based inspections - * because there's no system-level grant before install. - */ data class ApkInspection( - /** App label as the APK declares it (e.g. "Signal"). */ + val appLabel: String, val packageName: String, val versionName: String?, val versionCode: Long?, - /** SHA-256 of the APK signer cert, hex-encoded with colons. */ + val signingFingerprint: String?, - /** Lowest API the APK runs on. */ + val minSdk: Int?, - /** API the developer compiled against. */ + val targetSdk: Int?, - /** Permissions declared in the manifest, mapped with protection level. */ + val permissions: List, - /** Fully-qualified main launcher activity, if any. */ + val mainActivity: String?, - /** Total declared activity count (manifest entries). */ + val activityCount: Int, - /** Total declared service count. */ + val serviceCount: Int, - /** Total declared receiver count. */ + val receiverCount: Int, - /** Bytes on disk (file-based) or APK file size on the device (installed). */ + val fileSizeBytes: Long?, - /** Absolute path to the inspected APK file, if any. */ + val filePath: String?, - /** Manifest's `android:debuggable` flag — sketchy if true on a release build. */ + val debuggable: Boolean, - /** Source of the inspection. */ + val source: Source, ) { enum class Source { - /** Read from a file on disk that's parked for install. */ + FILE, - /** Read from an installed package's manifest. */ INSTALLED, } } data class ApkPermission( - /** Fully-qualified permission name (e.g. `android.permission.INTERNET`). */ + val name: String, - /** Short, user-friendly label loaded from the system or derived from [name]. */ + val displayName: String, - /** Description loaded from the system, when available. */ + val description: String?, val protectionLevel: ProtectionLevel, - /** - * Runtime grant state on the device. `true`/`false` for installed - * dangerous permissions; `null` for file-based inspections (no - * grant exists pre-install) and for normal-protection permissions - * that are auto-granted at install. - */ + val granted: Boolean?, ) -/** - * Coarse bucketing of Android's permission protection levels — enough - * to drive a "danger color" in the UI without exposing every flag bit. - */ enum class ProtectionLevel { - /** Auto-granted at install. Low risk. */ + NORMAL, - /** Sensitive — requires runtime user grant. Show in red. */ DANGEROUS, - /** Granted only to apps signed with the same cert. Show in amber. */ SIGNATURE, - /** Privileged platform permissions. Show in deep amber. */ PRIVILEGED, - /** Couldn't classify. */ UNKNOWN, } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppLanguage.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppLanguage.kt index 3acfed511..2628a7a64 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppLanguage.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppLanguage.kt @@ -1,29 +1,12 @@ package zed.rainxch.core.domain.model -/** - * A user-selectable UI language for the app. Each entry corresponds to a - * `values-` directory that ships with the Compose resources - * bundle, so the [tag] must match what the Android-style locale qualifier - * resolves to (e.g. `zh-rCN` → language tag `zh-CN`). - * - * [displayName] is intentionally hard-coded in the native script so the - * picker is readable regardless of the currently active UI language — a - * user stuck in the wrong language needs to recognise their own language - * to escape. - */ data class AppLanguage( - /** IETF BCP 47 language tag (e.g. `en`, `zh-CN`, `pt-BR`). */ + val tag: String, - /** Native-script label, e.g. `简体中文`, `Español`. */ + val displayName: String, ) -/** - * Registry of languages the app currently ships translations for. Keep - * in sync with `core/presentation/src/commonMain/composeResources/values-*` - * directories. Order is the order shown in the Tweaks picker (English - * first as the source-of-truth language, rest alphabetised by tag). - */ object AppLanguages { val ALL: List = listOf( diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt index ccd3a9ad2..69b34d578 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt @@ -1,13 +1,5 @@ package zed.rainxch.core.domain.model -/** - * Palette identity. Each entry maps 1:1 to a [zed.rainxch.core.presentation.theme.tokens.Tokens.Palette] - * at the presentation layer. Light / Dark / Amoled are orthogonal — see [ThemeMode]. - * - * Legacy values from the pre-overhaul enum (DYNAMIC / OCEAN / PURPLE / SLATE / AMBER) are - * migrated on first read by [fromName] — see [LEGACY_MIGRATION] for the explicit map. - * Material You dynamic color is intentionally dropped (themes.md "Disallowed combinations"). - */ enum class AppTheme { NORD, CREAM, @@ -16,7 +8,7 @@ enum class AppTheme { ; companion object { - /** Legacy → new palette mapping. Locked in `.design/DECISIONS.md` D3. */ + private val LEGACY_MIGRATION = mapOf( "DYNAMIC" to NORD, "OCEAN" to NORD, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ExportedApp.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ExportedApp.kt index 224c92035..5cd7d44e4 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ExportedApp.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ExportedApp.kt @@ -8,16 +8,12 @@ data class ExportedApp( val repoOwner: String, val repoName: String, val repoUrl: String, - // Monorepo tracking (added in export schema v2). Defaults keep - // old v1 JSON files decoding without changes. + val assetFilterRegex: String? = null, val fallbackToOlderReleases: Boolean = false, - // Preferred-variant tracking (added in export schema v3). Defaults - // keep older exports decoding without changes. + val preferredAssetVariant: String? = null, - // Multi-layer variant fingerprint (added in export schema v4): - // serialized token set, glob pattern, and same-position fallback - // metadata. All optional so older v1/v2/v3 exports still decode. + val preferredAssetTokens: String? = null, val assetGlobPattern: String? = null, val pickedAssetIndex: Int? = null, @@ -26,16 +22,7 @@ data class ExportedApp( @Serializable data class ExportedAppList( - /** - * Export schema version. - * - v2: added [ExportedApp.assetFilterRegex] / [ExportedApp.fallbackToOlderReleases] - * - v3: added [ExportedApp.preferredAssetVariant] - * - v4: added [ExportedApp.preferredAssetTokens] / [ExportedApp.assetGlobPattern] - * / [ExportedApp.pickedAssetIndex] / [ExportedApp.pickedAssetSiblingCount] - * - * All older versions still decode correctly because the new fields - * have safe defaults. - */ + val version: Int = 4, val exportedAt: Long = 0L, val apps: List = emptyList(), diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubReleaseExt.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubReleaseExt.kt index 315ccb043..5a5ee4697 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubReleaseExt.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubReleaseExt.kt @@ -2,43 +2,11 @@ package zed.rainxch.core.domain.model import zed.rainxch.core.domain.util.VersionMath -/** - * Single source of truth for "should this release be treated as a - * pre-release across the app". - * - * Combines: - * - [GithubRelease.isPrerelease] — the authoritative GitHub API flag. - * - [VersionMath.isPreReleaseTag] on [GithubRelease.tagName] — catches - * the common case where a maintainer publishes `v2.0.0-rc.1` but - * forgets to tick the "This is a pre-release" box. Without the - * tag heuristic, an opted-out user would be silently offered that - * build as if it were stable. - * - [VersionMath.isPreReleaseTag] on [GithubRelease.name] — some - * maintainers only put the `beta` marker in the human-readable - * release title (e.g. tag=`2.0.0`, name=`2.0.0 (beta)`). - * - * Every UI that shows a "Pre-release" badge and every filter that - * decides whether to surface a release to a given user MUST use this - * helper, otherwise the flag-vs-tag mismatch surfaces as a silent - * bug. - */ fun GithubRelease.isEffectivelyPreRelease(): Boolean = isPrerelease || VersionMath.isPreReleaseTag(tagName) || VersionMath.isPreReleaseTag(name) -/** - * Specific label for this release's pre-release marker — `"Beta"`, - * `"Alpha"`, `"RC"`, etc. — or `null` if no marker was detected. - * - * Tries the tag first (where the marker most often lives), falls - * back to the release name (some maintainers put the marker only - * in the title). Returns `null` when neither contains a recognised - * marker, in which case callers that still want to show a badge - * should check [isEffectivelyPreRelease] and fall back to a generic - * "Pre-release" pill — an opted-in API flag with no visible marker - * is still a pre-release, just one without a specific channel name. - */ fun GithubRelease.preReleaseLabel(): String? = VersionMath.preReleaseMarkerLabel(tagName) ?: VersionMath.preReleaseMarkerLabel(name) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HostToken.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HostToken.kt index 22d886393..30a72bb09 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HostToken.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HostToken.kt @@ -49,18 +49,6 @@ object HostNames { return noAuth.removeSuffix(".").removePrefix("www.").removePrefix("api.") } - /** - * Maps a request URL host to the canonical token storage host. - * - * GitHub stores the PAT under [GITHUB] (`github.com`), but real API - * requests go to `api.github.com` — without this collapse step the - * interceptor's `repo.get(request.url.host)` returns null on every - * GitHub REST call. - * - * Forgejo/Codeberg/Gitea instances expose their REST API at - * `https:///api/v1/...` (same host as the storage key), so - * they need no mapping. - */ fun sanitizePastedToken(raw: String): String { return raw.trim() .removePrefix("Bearer ") diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt index 3765b68d8..4d518e09d 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt @@ -34,109 +34,34 @@ data class InstalledApp( val latestVersionCode: Long? = null, val latestReleasePublishedAt: String? = null, val includePreReleases: Boolean = false, - /** - * Optional regex applied to asset names. When set, only assets whose - * names match the pattern are considered installable for this app — - * the building block for tracking one app inside a monorepo that ships - * multiple apps (e.g. `ente-auth.*` against `ente-io/ente`). - */ + val assetFilterRegex: String? = null, - /** - * When true, the update check walks back through past releases looking - * for one whose assets match [assetFilterRegex]. Required for monorepos - * where the latest release belongs to a sibling app. - */ + val fallbackToOlderReleases: Boolean = false, - /** - * Stable identifier for the asset variant (e.g. `arm64-v8a`, - * `universal`) that the user has chosen to track. Derived from the - * picked asset filename's tail (everything after the version) so it - * survives version bumps. `null` means "auto-pick by architecture". - */ + val preferredAssetVariant: String? = null, - /** - * Set when the update checker can't find an asset matching - * [preferredAssetVariant] in a fresh release — typically because the - * maintainer renamed or restructured the artefacts. The UI shows a - * "variant changed" prompt; the flag is cleared once the user picks - * a new variant. - */ + val preferredVariantStale: Boolean = false, - /** - * Token-set fingerprint of the picked asset, serialized via - * `AssetVariant.serializeTokens` (sorted, joined by `|`). Primary - * identity layer for the resolver — handles arch-before-version, - * OS-version interlopers, and counters between version and arch. - * - * `null` for older rows pinned before this column existed and for - * filenames where the token vocabulary recognises nothing. - */ + val preferredAssetTokens: String? = null, - /** - * Glob-pattern fingerprint of the picked asset (e.g. - * `app-*-arm64-v8a.apk`). Secondary identity layer used when the - * token vocabulary doesn't recognise anything in the filename — - * the most common case being custom flavor names. - */ + val assetGlobPattern: String? = null, - /** - * Zero-based index of the picked asset in the original release's - * installable-asset list. Last-resort same-position fallback when - * none of the fingerprint layers match in a fresh release. - */ + val pickedAssetIndex: Int? = null, - /** - * Total installable assets in the release the user picked from. - * Pairs with [pickedAssetIndex] for the same-position fallback. - */ + val pickedAssetSiblingCount: Int? = null, - /** - * Absolute path to a downloaded asset that's waiting for the user - * to confirm install. Non-null means: the orchestrator finished - * the download in `InstallWhileForeground` mode but the foreground - * screen had gone away by then, so the bytes are parked and the - * apps list shows a "ready to install" row. - * - * Cleared by the orchestrator after a successful install or when - * the user dismisses the row. - */ + val pendingInstallFilePath: String? = null, - /** - * Release tag of the version represented by [pendingInstallFilePath]. - * Used by Details to detect "the parked file matches the - * currently-selected release" and skip re-downloading on install. - */ + val pendingInstallVersion: String? = null, - /** - * Original (unscoped) asset filename of the parked file. Pairs - * with [pendingInstallVersion] for the Details-screen match. - */ + val pendingInstallAssetName: String? = null, - /** - * Release tag the user explicitly skipped via the apps row "Skip - * this version" action. While non-null, the periodic update check - * suppresses the badge for [packageName] when the matched release - * tag equals this value. Auto-clears the moment a strictly newer - * release lands so the user gets re-notified next cycle. - */ + val skippedReleaseTag: String? = null, val sourceHost: String? = null, ) -/** - * True when the app actually exists on device. A row with - * [InstalledApp.isPendingInstall] set means the bytes are parked on disk - * but the system install has not completed (or failed) — callers that - * surface an "Installed" badge must treat that case as *not* installed, - * otherwise the Details screen (which checks `isPendingInstall`) and the - * Home/Search cards drift out of sync after a failed install. - */ fun InstalledApp?.isReallyInstalled(): Boolean = this != null && !this.isPendingInstall -/** - * True when a genuine update is pending install — mirrors the check - * [zed.rainxch.details.presentation.components.SmartInstallButton] does - * so non-Details surfaces render the same state machine. - */ fun InstalledApp?.hasActualUpdate(): Boolean = this != null && this.isUpdateAvailable && !this.isPendingInstall diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallerCategory.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallerCategory.kt index 739368a34..e9d2a1d9c 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallerCategory.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstallerCategory.kt @@ -34,12 +34,6 @@ enum class InstallerCategory(val sortPriority: Int) { "com.amazon.venezia", ) - // Heuristic fallback when the installer is null/unknown — many - // OEM-preloaded apps live in /data (so they aren't flagged - // FLAG_UPDATED_SYSTEM_APP) and report an installer string we - // don't recognize or none at all due to package-visibility - // quirks. Falling back to the package-name namespace keeps them - // out of the SIDELOADED bucket where they'd drown user content. private val VENDOR_PACKAGE_PREFIXES = listOf( "com.samsung.", diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt index 1da13af3a..b7ed6f5ce 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt @@ -1,24 +1,5 @@ package zed.rainxch.core.domain.model -/** - * Independent proxy "channels" — each scope has its own configurable - * [ProxyConfig] so users can, for example, route GitHub API traffic - * through a corporate proxy while keeping APK downloads direct. - * - * Every outbound request in the app belongs to exactly one scope: - * - * - [DISCOVERY] — GitHub REST API calls: search, home, details, - * repo metadata, user profiles, starred, installed - * apps update checks. Basically everything that - * hits `api.github.com`. - * - [DOWNLOAD] — APK file downloads: manual installs from Details, - * one-tap updates from the Installed Apps list, and - * the Android auto-update worker. - * - [TRANSLATION] — README translation requests (currently Google - * Translate). Kept separate because translation - * services are often blocked/unblocked independently - * of GitHub in restricted networks. - */ enum class ProxyScope { DISCOVERY, DOWNLOAD, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemArchitecture.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemArchitecture.kt index accefdb56..8d7121ac7 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemArchitecture.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/SystemArchitecture.kt @@ -1,10 +1,10 @@ package zed.rainxch.core.domain.model enum class SystemArchitecture { - X86_64, // Intel/AMD 64-bit - AARCH64, // ARM 64-bit - X86, // Intel/AMD 32-bit - ARM, // ARM 32-bit + X86_64, + AARCH64, + X86, + ARM, UNKNOWN, ; diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt index 9380460a9..ac5d9a46e 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ThemeMode.kt @@ -1,10 +1,5 @@ package zed.rainxch.core.domain.model -/** - * Light / Dark / Amoled / System — orthogonal to [AppTheme] palette. AMOLED is a Dark - * sub-mode (pure-black background); falls back to DARK rendering when SYSTEM resolves - * to dark and AMOLED preference is enabled separately. - */ enum class ThemeMode { LIGHT, DARK, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt index 0ddd30003..414289899 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt @@ -1,17 +1,5 @@ package zed.rainxch.core.domain.model -/** - * Backend used to translate README content. - * - * - [GOOGLE] hits Google's public `translate.googleapis.com/translate_a/single` - * endpoint. No credentials required and works everywhere Google does, but - * it's an undocumented endpoint that can change or rate-limit at any time, - * and it's unreachable from mainland China without a proxy. - * - [YOUDAO] hits Youdao's official `openapi.youdao.com/api`. Requires the - * user to provide their own `appKey` / `appSecret` from Youdao's developer - * portal (there's no anonymous free tier). Directly accessible from - * mainland China, which is why it exists — see issue #429. - */ enum class TranslationProvider { GOOGLE, YOUDAO, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/DigestVerifier.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/DigestVerifier.kt index f81c6093b..5bba5212c 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/DigestVerifier.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/DigestVerifier.kt @@ -1,13 +1,7 @@ package zed.rainxch.core.domain.network interface DigestVerifier { - /** - * Streams the file at [filePath] through SHA-256 and compares against - * [expectedDigest] (which may carry a `sha256:` prefix or be raw hex). - * - * @return null on match, a non-null human-readable reason on - * mismatch / IO error. - */ + suspend fun verify( filePath: String, expectedDigest: String, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt index 076789b01..88438bd96 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt @@ -2,41 +2,30 @@ package zed.rainxch.core.domain.network import zed.rainxch.core.domain.model.ProxyConfig -/** - * Verifies that a [ProxyConfig] can actually reach the GitHub API. Implementations - * should issue a single lightweight request through a throwaway HTTP client built - * with the supplied config so the test exercises the same engine code path the - * real client uses. - */ interface ProxyTester { suspend fun test(config: ProxyConfig): ProxyTestOutcome } sealed interface ProxyTestOutcome { - /** Connection succeeded. [latencyMs] is the round-trip time of the test request. */ + data class Success( val latencyMs: Long, ) : ProxyTestOutcome sealed interface Failure : ProxyTestOutcome { - /** Could not resolve a hostname (DNS failure or unresolved proxy host). */ + data object DnsFailure : Failure - /** Reached the network but could not connect to the proxy itself. */ data object ProxyUnreachable : Failure - /** Connection or socket timed out. */ data object Timeout : Failure - /** Proxy returned 407 / requested authentication. */ data object ProxyAuthRequired : Failure - /** Proxy or upstream returned a non-2xx HTTP status. */ data class UnexpectedResponse( val statusCode: Int, ) : Failure - /** Anything else (TLS errors, malformed config, etc.). */ data class Unknown( val message: String?, ) : Failure diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt index 2dcbdfe77..cc20a729e 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt @@ -45,16 +45,6 @@ interface InstalledAppsRepository { suspend fun updateApp(app: InstalledApp) - /** - * Atomically writes only the installed-version columns + the - * `isUpdateAvailable` flag for [packageName]. Prefer this over - * [updateApp] on hot paths where the caller holds a possibly-stale - * snapshot and only wants to persist a version change — full-row - * updates from stale snapshots can clobber concurrent writes to - * sibling columns (download orchestrator, variant pin, favourite - * toggle, periodic update check). Introduced for the external - * install path (`PackageEventReceiver`). - */ suspend fun updateInstalledVersion( packageName: String, installedVersion: String, @@ -78,42 +68,12 @@ interface InstalledAppsRepository { enabled: Boolean, ) - /** - * Persists per-app monorepo settings: an optional regex applied to asset - * names and whether the update checker should fall back to older - * releases when the latest one has no matching asset. - * - * Implementations should re-check the app for updates immediately so - * the UI reflects the new state without a manual refresh. - */ suspend fun setAssetFilter( packageName: String, regex: String?, fallbackToOlderReleases: Boolean, ) - /** - * Persists the user's preferred asset variant for [packageName] - * along with the full multi-layer fingerprint: - * - * - [variant]: legacy substring-tail label, used as the display - * name and as a third-tier match in the resolver - * - [tokens]: serialized token-set fingerprint (primary identity) - * - [glob]: glob-pattern fingerprint (secondary identity) - * - [pickedIndex]: zero-based index of the picked asset in the - * release's installable-asset list (same-position fallback) - * - [siblingCount]: total installable assets in the picked release - * - * Always clears the `preferredVariantStale` flag in the same write - * because the user has just made an explicit choice. - * - * Pass `null` for all fields except [packageName] to unpin and fall - * back to the platform auto-picker — convenient via [clearPreferredVariant]. - * - * Implementations should re-check the app for updates immediately - * so the cached `latestAsset*` fields point at the variant the user - * just selected, without waiting for the next periodic worker. - */ suspend fun setPreferredVariant( packageName: String, variant: String?, @@ -123,53 +83,15 @@ interface InstalledAppsRepository { siblingCount: Int? = null, ) - /** - * Convenience for [setPreferredVariant] that clears every - * fingerprint layer for [packageName] in a single call. The - * resolver will fall back to the platform auto-picker on the next - * update check. - */ suspend fun clearPreferredVariant(packageName: String) - /** - * Marks [tag] as skipped for [packageName] so the periodic update - * check stops surfacing it as an available update. Pass `null` for - * [tag] to clear an existing skip. - * - * Implementations also clear the row's `isUpdateAvailable` flag in - * the same write when [tag] is non-null so the apps list drops the - * badge immediately. The skip auto-clears the moment a strictly - * newer release lands; users do not have to unskip manually for a - * future bump. - */ suspend fun setSkippedReleaseTag( packageName: String, tag: String?, ) - /** - * Live stream of every installed app whose - * [InstalledApp.skippedReleaseTag] is non-null. Backs the Tweaks - * "Skipped updates" sub-screen; emits `[]` when no app is currently - * skipping a release. - */ fun getAppsWithSkippedReleaseTag(): Flow> - /** - * Sets (or clears) the path + version + asset name of a - * downloaded-but-not-yet-installed asset for [packageName]. - * - * Used by `DefaultDownloadOrchestrator` when an - * `InstallPolicy.InstallWhileForeground` download completes - * after the foreground screen has been destroyed — the file is - * parked, these three columns are set, and the apps list shows - * a "Ready to install" row. The Details screen also uses - * [version] + [assetName] to detect "the parked file matches - * the currently-selected release" and skip re-downloading. - * - * Pass `null` for all three to clear (after a successful install - * or after the user dismissed the row). - */ suspend fun setPendingInstallFilePath( packageName: String, path: String?, @@ -177,17 +99,6 @@ interface InstalledAppsRepository { assetName: String? = null, ) - /** - * Dry-run helper for the per-app advanced settings sheet. Fetches a - * window of releases for [owner]/[repo] (honouring [includePreReleases]) - * and returns the assets in the most-recent release that match - * [regex] — or, if [fallbackToOlderReleases] is true and the latest - * release matches nothing, the assets from the next release that does. - * - * Returns an empty list when no matching release is found in the - * window. Never throws — failures resolve to an empty list and are - * logged at debug level. - */ suspend fun previewMatchingAssets( owner: String, repo: String, @@ -199,10 +110,6 @@ interface InstalledAppsRepository { suspend fun executeInTransaction(block: suspend () -> R): R } -/** - * Snapshot returned by [InstalledAppsRepository.previewMatchingAssets] for - * the per-app advanced settings sheet's live preview. - */ data class MatchingPreview( val release: GithubRelease?, val matchedAssets: List, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt index f048fea65..f38784d2c 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt @@ -5,25 +5,15 @@ import zed.rainxch.core.domain.model.MirrorConfig import zed.rainxch.core.domain.model.MirrorPreference interface MirrorRepository { - /** - * Emits the cached catalog immediately, then fresh entries on each - * successful refresh. Falls back to the bundled list when the cache - * is empty and the backend is unreachable. - */ + fun observeCatalog(): Flow> - /** Forces a backend fetch ignoring the 24h cache. */ suspend fun refreshCatalog(): Result fun observePreference(): Flow suspend fun setPreference(pref: MirrorPreference) - /** - * Emits a one-shot notice when the user's previously-selected mirror - * disappears from a freshly-fetched catalog and the repository - * auto-falls-back to Direct. UI surfaces a toast. - */ fun observeRemovedNotices(): Flow suspend fun snoozeAutoSuggest(forMs: Long) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 85153c8fd..7b8db7574 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -111,31 +111,14 @@ interface TweaksRepository { suspend fun setMicrosoftTranslatorRegion(region: String) - /** - * Selected UI language as a BCP 47 tag (e.g. `zh-CN`). Emits - * `null` when the user hasn't picked one — which means "follow - * whatever the JVM/Android locale is" at app start. `null` is - * distinct from `""`: the former is the unset state, the latter - * would be a malformed user choice we don't support. - */ fun getAppLanguage(): Flow suspend fun setAppLanguage(tag: String?) - /** - * When `true`, Details kicks off a translation of the README and - * release notes immediately on load, using - * [getAutoTranslateTargetLang] as the target. Default `false`. - */ fun getAutoTranslateEnabled(): Flow suspend fun setAutoTranslateEnabled(enabled: Boolean) - /** - * BCP 47 tag of the language Details auto-translates into when - * [getAutoTranslateEnabled] is `true`. `null` falls back to the - * UI language ([getAppLanguage]) at translate time. - */ fun getAutoTranslateTargetLang(): Flow suspend fun setAutoTranslateTargetLang(tag: String?) @@ -152,50 +135,22 @@ interface TweaksRepository { suspend fun setExternalImportBannerDismissedAtCount(count: Int) - /** - * Permanent dismissal flag for the Keep Android Open campaign banner on - * Apps. False until user taps the close button; flips to true forever - * once dismissed. - */ fun getKaoBannerDismissed(): Flow suspend fun setKaoBannerDismissed(dismissed: Boolean) - /** - * One-shot flag for the APK Inspect coachmark next to the install - * button on the details screen. `false` until the user has seen the - * coachmark at least once; flips permanently to `true` thereafter. - */ fun getApkInspectCoachmarkShown(): Flow suspend fun setApkInspectCoachmarkShown(shown: Boolean) - /** - * One-shot flag for the release-channel coachmark on the Details - * screen. Survey signal — users don't realise the per-app channel - * chip toggles betas. `false` until shown at least once; permanent - * `true` after dismissal. - */ fun getChannelChipCoachmarkShown(): Flow suspend fun setChannelChipCoachmarkShown(shown: Boolean) - /** - * When true, the release-assets picker on Details shows installers - * for every OS (grouped by platform section). When false (default), - * only assets installable on the current platform are listed. - */ fun getShowAllPlatforms(): Flow suspend fun setShowAllPlatforms(enabled: Boolean) - /** - * One-shot watermark for the battery-optimization prompt on - * aggressive-OEM ROMs (Oppo / OnePlus / Realme / Xiaomi / vivo / - * Honor). `false` until the user has either granted the exemption - * or explicitly dismissed the prompt; flips to `true` afterwards - * and is never re-shown. - */ fun getBatteryOptimizationPromptDismissed(): Flow suspend fun setBatteryOptimizationPromptDismissed(dismissed: Boolean) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/ApkInspector.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/ApkInspector.kt index f49e52b9e..695fa8099 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/ApkInspector.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/ApkInspector.kt @@ -2,27 +2,9 @@ package zed.rainxch.core.domain.system import zed.rainxch.core.domain.model.ApkInspection -/** - * Reads metadata out of an APK file or an installed package and returns - * the result as an [ApkInspection]. Used by the "Inspect APK" sheet to - * show users what a package declares before they install it (and to - * audit installed packages after the fact). - * - * Implementations live per-platform; on Android the Inspector reads - * from `PackageManager`, on JVM/desktop it returns `null` (no APK - * concept on those targets). - */ interface ApkInspector { - /** - * Inspects an APK file at [filePath]. Returns `null` if the file - * doesn't exist, isn't a valid APK, or the platform can't extract - * metadata. - */ + suspend fun inspectFile(filePath: String): ApkInspection? - /** - * Inspects an installed package by [packageName]. Returns `null` - * if the package isn't on the system or metadata is unavailable. - */ suspend fun inspectInstalled(packageName: String): ApkInspection? } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt index 8f468f211..66eff0217 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadOrchestrator.kt @@ -4,183 +4,40 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import zed.rainxch.core.domain.model.GithubAsset -/** - * Application-scoped manager for in-flight downloads and installs. - * - * # Why this exists - * - * Before this interface, every download/install ran inside the calling - * ViewModel's `viewModelScope`. Two problems followed: - * - * 1. **Screen-leave kills installs** — navigating away from the - * details screen mid-download cancels the OkHttp call and deletes - * the partial file. Users had to camp on the screen to update - * anything. - * - * 2. **No cross-screen coordination** — the apps list and the details - * screen each had their own download bookkeeping. Hopping between - * screens lost progress, status, and cancel handles. - * - * The orchestrator solves both by owning a long-lived [CoroutineScope] - * tied to the application's lifetime. ViewModels become **observers** - * of [downloads] rather than **owners** of any work — when a screen is - * destroyed, the work keeps running and the next screen instance picks - * the state right back up. - * - * # Install policies - * - * Each enqueued download declares an [InstallPolicy] that controls what - * happens when the bytes are on disk: - * - * - [InstallPolicy.AlwaysInstall] — fire the installer no matter what. - * Used for **Shizuku** silent installs, where there's no UI dialog - * and the user has explicitly opted into the unattended path. - * - * - [InstallPolicy.InstallWhileForeground] — fire the installer **only - * if the screen that requested the download is still alive**. The - * ViewModel must call [downgradeToDeferred] in `onCleared()`. If the - * downgrade lands before the download completes, the installer is - * not invoked; instead the file is parked, the row is marked - * "pending install", and the notifier is poked. - * - * - [InstallPolicy.DeferUntilUserAction] — never auto-install. Used by - * the apps list when the user has tapped "Update" but the screen - * might leave at any moment, and by recovery flows. The file lands - * on disk and the user installs it explicitly. - * - * # Concurrency model - * - * Up to [maxConcurrent] downloads run in parallel; further `enqueue` - * calls join an internal queue. Cancellation is per-package: cancelling - * one download doesn't touch the others. Implementations should - * surface a `SupervisorJob` so a single failed download can't poison - * the scope. - * - * # Filename namespacing - * - * The orchestrator scopes filenames as `owner_repo_originalName.ext` - * before passing them to the [zed.rainxch.core.domain.network.Downloader] - * — this prevents collisions between two repos that ship installers - * with the same name (e.g. `app.apk`). - */ interface DownloadOrchestrator { - /** - * Live snapshot of every download the orchestrator currently knows - * about, keyed by package name. Includes downloads in every stage - * (queued, downloading, awaiting install, failed, cancelled). The - * map is updated atomically so consumers can use plain `collect` - * without worrying about torn reads. - * - * Entries are removed when: - * - The install completes (foreground or Shizuku) and - * [dismiss] is called by the consuming ViewModel - * - The user explicitly dismisses a failed/cancelled download - * - The orchestrator is cleared (process death) - * - * Pending-install entries (`stage = AwaitingInstall`) survive - * across screen instances precisely so the apps list can show - * "ready to install" rows. - */ + val downloads: StateFlow> - /** - * Convenience flow that emits the entry for [packageName] whenever - * it changes — equivalent to `downloads.map { it[packageName] }` - * with `distinctUntilChanged`. Use this from a ViewModel observing - * a single app. - */ fun observe(packageName: String): Flow - /** - * Enqueues a download for [spec]. If a download for the same - * package is already in progress, this returns the existing - * download's id without starting a new one — duplicates are a - * silent no-op so consumers can be naïve about idempotency. - * - * @return the orchestrator's internal id for the download. Use it - * with [cancel] / [dismiss] for safe targeting (the package name - * also works but is less precise if a row gets re-enqueued). - */ suspend fun enqueue(spec: DownloadSpec): String - /** - * Atomically swaps the install policy for [packageName] to - * [InstallPolicy.DeferUntilUserAction]. Called by foreground - * ViewModels in `onCleared()` so a download that was started with - * [InstallPolicy.InstallWhileForeground] doesn't auto-install - * after the screen goes away. - * - * Race-safe: if the download has already finished and the install - * is in progress, this is a no-op (the install completes). If the - * download is still running, the policy flips immediately and the - * orchestrator parks the file when bytes are done. - * - * No-op if no download exists for [packageName]. - */ suspend fun downgradeToDeferred(packageName: String) - /** - * Cancels the download for [packageName] (if any), deletes the - * partial file, and removes the entry from [downloads]. Safe to - * call from any thread; idempotent. - */ suspend fun cancel(packageName: String) - /** - * Triggers the install of a previously-deferred download. Looks up - * the file via [OrchestratedDownload.filePath] for [packageName], - * runs the platform installer, and removes the entry on success. - * - * Used by: - * - The apps list "Install" button when a row is in - * [DownloadStage.AwaitingInstall] - * - The notification action that opens the apps list and resumes - * - * Validation (signing fingerprint, package mismatch) is the - * caller's responsibility — the orchestrator runs the bare - * `installer.install` call. The dialog-driven path in - * `DetailsViewModel` continues to handle the screen-attached - * case where the validation surface lives. - */ suspend fun installPending(packageName: String): InstallOutcome? - /** - * Removes the entry for [packageName] from [downloads]. Used when - * the consuming ViewModel has handled a terminal state (success, - * error, cancellation) and doesn't want it sticking around in the - * map. - */ fun dismiss(packageName: String) } -/** - * What the caller wants the orchestrator to do with a single asset. - */ data class DownloadSpec( - /** Package name being installed/updated. Used as the map key. */ + val packageName: String, - /** Repository owner — half of the filename namespace prefix. */ + val repoOwner: String, - /** Repository name — the other half. */ + val repoName: String, - /** Asset to download. The orchestrator pulls URL/name/size from this. */ + val asset: GithubAsset, - /** Display name shown in notifications and progress UI. */ + val displayAppName: String, - /** What to do with the file once it's on disk. See [InstallPolicy]. */ + val installPolicy: InstallPolicy, - /** - * Release tag of the version being downloaded. Stored on the - * orchestrator entry for log / display purposes; the orchestrator - * itself doesn't interpret it. - */ + val releaseTag: String, ) -/** - * Snapshot of one download's state in [DownloadOrchestrator.downloads]. - * Immutable — every state transition produces a new instance. - */ data class OrchestratedDownload( val id: String, val packageName: String, @@ -191,85 +48,44 @@ data class OrchestratedDownload( val assetSize: Long, val downloadUrl: String, val releaseTag: String, - /** Path on disk once the download has completed. Null until then. */ + val filePath: String?, - /** Current install policy — can change mid-flight via downgrade. */ + val installPolicy: InstallPolicy, val stage: DownloadStage, - /** 0..100, null when content-length is unknown. */ + val progressPercent: Int?, - /** - * Bytes received so far. Updated on every emission of the - * underlying download flow — gives the UI a smooth byte counter - * even when the percent integer hasn't ticked over. - */ + val bytesDownloaded: Long = 0L, - /** - * Total bytes expected. Falls back to [assetSize] when the - * server doesn't send `Content-Length`. Null only in rare cases - * where neither is known up front. - */ + val totalBytes: Long? = null, - /** Error message if [stage] is [DownloadStage.Failed]. */ + val errorMessage: String? = null, val installOutcome: InstallOutcome? = null, ) -/** - * Lifecycle stages a download moves through. Forward-only except via - * [DownloadOrchestrator.cancel] which can interrupt anything before - * `Completed`. - */ enum class DownloadStage { - /** Sitting in the queue, waiting for a worker slot. */ + Queued, - /** OkHttp is actively pulling bytes. */ Downloading, - /** Bytes are on disk; the orchestrator is about to invoke the installer. */ Installing, - /** - * Bytes are on disk and the install policy said "wait for the user". - * The apps list shows a one-tap install button for entries in this - * stage. The notifier was poked when the entry first landed here. - */ AwaitingInstall, - /** Installer reported success. The entry is eligible for [DownloadOrchestrator.dismiss]. */ Completed, - /** Cancelled by [DownloadOrchestrator.cancel]. */ Cancelled, - /** Download or install failed. See [OrchestratedDownload.errorMessage]. */ Failed, } -/** - * What the orchestrator should do with the bytes once they're on disk. - * See the interface kdoc for the rationale behind each policy. - */ enum class InstallPolicy { - /** - * Always invoke the installer when the download finishes. Used - * for Shizuku silent installs and other unattended paths. - */ + AlwaysInstall, - /** - * Invoke the installer only if the screen that requested the - * download is still attached. Foreground ViewModels declare this - * and call [DownloadOrchestrator.downgradeToDeferred] in - * `onCleared()`. - */ InstallWhileForeground, - /** - * Never auto-install. The file is parked in - * [DownloadStage.AwaitingInstall] and the user installs from the - * apps list explicitly. - */ DeferUntilUserAction, } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadProgressNotifier.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadProgressNotifier.kt index 1709e08ad..eb0176f33 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadProgressNotifier.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/DownloadProgressNotifier.kt @@ -1,40 +1,7 @@ package zed.rainxch.core.domain.system -/** - * Surfaces live download progress in the system notification shade so - * users can track downloads while the app is in the background (see - * GitHub-Store#373). - * - * Lifecycle: [DownloadOrchestrator] is the single source of truth for - * download state. A platform observer subscribes to - * [DownloadOrchestrator.downloads] and calls [notifyProgress] on every - * `Queued` / `Downloading` emission, and [clearProgress] on any terminal - * or install-stage transition. The orchestrator itself does not know - * about this notifier — wiring is one-way. - * - * Platform contracts: - * - **Android**: persistent ongoing notification with progress bar and - * a "Cancel" action that broadcasts back to - * `DownloadCancelReceiver`. Channel id `app_downloads`. - * - **JVM/Desktop**: no-op. Desktop downloads happen with the window - * visible and installers complete synchronously via the OS dialog; - * there is no equivalent of the Android notification shade to target. - */ interface DownloadProgressNotifier { - /** - * Posts or updates the progress notification for [packageName]. - * Safe to call on every progress tick — the Android impl uses - * `setOnlyAlertOnce(true)` so the notification updates silently. - * - * @param packageName Stable key; also drives the notification id. - * @param appName User-visible title. - * @param versionTag Release tag shown alongside byte counts. - * @param percent 0..100, or `null` when the server did not send - * a `Content-Length` header (indeterminate spinner). - * @param bytesDownloaded Bytes received so far — shown in the - * notification's content text. - * @param totalBytes Expected total, or `null` if unknown. - */ + fun notifyProgress( packageName: String, appName: String, @@ -44,10 +11,5 @@ interface DownloadProgressNotifier { totalBytes: Long?, ) - /** - * Dismisses the progress notification for [packageName]. Called on - * any transition out of `Queued` / `Downloading` — including - * `AwaitingInstall`, which is owned by [PendingInstallNotifier]. - */ fun clearProgress(packageName: String) } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt index 9ab0a99e0..44e6ccddb 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt @@ -3,22 +3,10 @@ package zed.rainxch.core.domain.system import zed.rainxch.core.domain.model.GithubAsset import zed.rainxch.core.domain.model.SystemArchitecture -/** - * Result of an [Installer.install] call. - */ enum class InstallOutcome { - /** - * Installation completed synchronously (e.g. Shizuku silent install). - * The package is already installed on the system — no need to wait - * for a broadcast to confirm. - */ + COMPLETED, - /** - * Installation was handed off to the system UI or an external process. - * The caller should treat the install as pending until a - * PACKAGE_ADDED / PACKAGE_REPLACED broadcast confirms it. - */ DELEGATED_TO_SYSTEM, } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/PendingInstallNotifier.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/PendingInstallNotifier.kt index 0dbb37f0f..d487b7621 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/PendingInstallNotifier.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/PendingInstallNotifier.kt @@ -1,38 +1,7 @@ package zed.rainxch.core.domain.system -/** - * Surfaces "your download is ready, tap to install" notifications. - * - * Used by [DownloadOrchestrator] when an [InstallPolicy.InstallWhileForeground] - * download finishes after the foreground screen has been destroyed - * (the user navigated away mid-download). The notification gives them - * a one-tap path back into the app to complete the install. - * - * Platform contracts: - * - **Android**: real notification with a deep link into the Details - * page for the specific app (`githubstore://repo/owner/name`). - * Falls back to the apps tab (`githubstore://apps`) when owner/repo - * are unavailable. Channel id `app_updates`. - * - **JVM/Desktop**: no-op (Desktop doesn't background-install APKs; - * the apps tab is hidden on Desktop anyway). - */ interface PendingInstallNotifier { - /** - * Posts (or updates) the notification for [packageName]. - * - * @param packageName Used as a stable notification id so multiple - * pending installs each get their own row instead of replacing - * each other. - * @param repoOwner GitHub owner — used to build the - * `githubstore://repo/owner/name` deep link so tapping the - * notification opens the Details page for *this specific app*. - * @param repoName GitHub repo — paired with [repoOwner] for the - * deep link. - * @param appName User-visible app name shown as the notification - * title. - * @param versionTag Release tag shown as the notification body - * (e.g. "v1.2.3"). - */ + fun notifyPending( packageName: String, repoOwner: String, @@ -41,10 +10,5 @@ interface PendingInstallNotifier { versionTag: String, ) - /** - * Dismisses the pending-install notification for [packageName]. - * Called by the orchestrator when the user installs the file - * (either from the apps row or from the notification action). - */ fun clearPending(packageName: String) } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/RepoMatchResult.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/RepoMatchResult.kt index 2cb3f2768..c229be1cd 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/RepoMatchResult.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/RepoMatchResult.kt @@ -7,10 +7,7 @@ data class RepoMatchSuggestion( val source: RepoMatchSource, val stars: Int? = null, val description: String? = null, - // Non-null when the suggestion lives on a non-GitHub forge - // (Codeberg / Forgejo / custom). Drives the URL builder in the - // "select suggestion" path so we don't navigate to github.com for - // a Forgejo repo. + val sourceHost: String? = null, ) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/SystemInstallSerializer.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/SystemInstallSerializer.kt index dcb330b6a..233634cd2 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/SystemInstallSerializer.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/SystemInstallSerializer.kt @@ -9,14 +9,7 @@ interface SystemInstallSerializer { fun markCompleted(packageName: String) companion object { - // Tuned down from 60s after field reports of "stuck at 100%" - // when a prior install returned DELEGATED_TO_SYSTEM and the - // broadcast that releases the gate never arrived (user - // dismissed the system dialog, OEM throttling, Shizuku - // fallback to default installer with no follow-through). - // 15s is long enough to cover a normal Shizuku/Dhizuku silent - // install round-trip and short enough that the queue recovers - // before the user gives up. + const val DEFAULT_TIMEOUT_MS: Long = 15_000L } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt index 061806a78..d36f87342 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt @@ -1,19 +1,8 @@ package zed.rainxch.core.domain.system -/** - * Abstraction for rescheduling background update checks. - * Android implementation delegates to WorkManager; Desktop is a no-op. - */ interface UpdateScheduleManager { - /** - * Reschedules the periodic update check with a new interval. - * Takes effect immediately (replaces existing schedule). - */ + fun reschedule(intervalHours: Long) - /** - * Cancels every pending update-check / auto-update worker. Used - * when the user disables background update checking entirely. - */ fun cancel() } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt index 04499653b..3dd70ee85 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt @@ -9,18 +9,6 @@ import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.system.PackageMonitor -/** - * Use case for synchronizing installed apps state with the system package manager. - * - * Responsibilities: - * 1. Remove apps from DB that are no longer installed on the system - * 2. Migrate legacy apps missing versionName/versionCode fields - * 3. Resolve pending installs once they appear in the system package manager - * 4. Clean up stale pending installs (older than 24 hours) - * 5. Detect external version changes (downgrades on rooted devices, sideloads, etc.) - * - * This should be called before loading or refreshing app data to ensure consistency. - */ class SyncInstalledAppsUseCase( private val packageMonitor: PackageMonitor, private val installedAppsRepository: InstalledAppsRepository, @@ -28,7 +16,7 @@ class SyncInstalledAppsUseCase( private val logger: GitHubStoreLogger, ) { companion object { - private const val PENDING_TIMEOUT_MS = 24 * 60 * 60 * 1000L // 24 hours + private const val PENDING_TIMEOUT_MS = 24 * 60 * 60 * 1000L } suspend operator fun invoke(): Result = @@ -43,13 +31,7 @@ class SyncInstalledAppsUseCase( val toResolvePending = mutableListOf() val toDeleteStalePending = mutableListOf() val toSyncVersions = mutableListOf() - // Rows that are confirmed-installed but still carry a - // stale `pendingInstallFilePath`. Happens when the user - // installed a parked file but the broadcast handler - // missed the cleanup (process killed mid-install, - // legacy rows from before the cleanup was wired in, - // etc.). Without this sweep the apps screen keeps - // rendering an "Install" CTA forever. + val toClearStaleParkedFile = mutableListOf() appsInDb.forEach { app -> @@ -72,7 +54,6 @@ class SyncInstalledAppsUseCase( toMigrate.add(app.packageName to migrationResult) } - // Detect external version changes (downgrades on rooted devices, sideloads, etc.) isOnSystem && platform == Platform.ANDROID -> { toSyncVersions.add(app) } @@ -106,12 +87,7 @@ class SyncInstalledAppsUseCase( val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) if (systemInfo != null) { val latestVersionCode = app.latestVersionCode ?: 0L - // Also pin `installedVersion` (tag) to the - // intended new release. Skipping it leaves - // checkForUpdates re-flagging the row as - // updatable on every sweep because the - // tag-string compare keeps seeing the old - // version (#515). + val resolvedTag = app.latestVersion ?: systemInfo.versionName installedAppsRepository.updateApp( app.copy( @@ -129,11 +105,7 @@ class SyncInstalledAppsUseCase( installedAppsRepository.updatePendingStatus(app.packageName, false) logger.info("Resolved pending install (no system info): ${app.packageName}") } - // Resolution implies the system holds the - // package — drop the parked-file metadata - // so the apps row stops advertising an - // Install CTA on a file the user already - // installed. + installedAppsRepository.setPendingInstallFilePath( packageName = app.packageName, path = null, diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFileName.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFileName.kt index 9a39ef40f..059661cb0 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFileName.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFileName.kt @@ -1,64 +1,11 @@ package zed.rainxch.core.domain.util -/** - * Builds a filename safe for the local downloads directory that's also - * unique across repositories. - * - * # Why this exists - * - * Two different repos can ship installers with the same filename - * (`app.apk`, `release.apk`, `installer.exe` are all common). Without - * a per-repo namespace prefix, downloading both clobbers the first - * file, breaks the orchestrator's "active downloads by filename" - * tracking, and can install the wrong APK on top of the right row. - * - * # Output format - * - * __. - * - * Examples: - * - * ente-io / ente / ente-auth-3.2.5-arm64-v8a.apk - * → ente-io_ente_ente-auth-3.2.5-arm64-v8a.apk - * - * APKMirror / SomeApp / app.apk - * → apkmirror_someapp_app.apk - * - * d4rk7355608 / Apps_Manager / Apps Manager-v1.0.apk - * → d4rk7355608_apps_manager_apps_manager-v1.0.apk - * - * # Sanitization - * - * Every component is independently sanitized to: - * - Lowercase (filesystem case-folding portability — APFS folds, ext4 - * doesn't, NTFS varies — folding eagerly avoids cross-platform - * surprises) - * - Strip path traversal: `..`, `/`, `\` - * - Replace whitespace and shell-unfriendly characters with `_` - * - Collapse runs of `_` - * - * The original extension is preserved separately so the system - * installer's MIME-type detection still works (`.apk`, `.exe`, `.dmg`, - * etc.). - */ object AssetFileName { - /** - * Characters that need replacement. Path separators must go; - * everything else is for sanity (whitespace, shell metacharacters, - * Windows-illegal chars `< > : " | ? *`). The output keeps `-` and - * letters/digits intact because those are the most common tokens - * in real filenames. - */ + private val FORBIDDEN = Regex("""[^a-z0-9.\-]""") private val MULTI_UNDERSCORE = Regex("_+") - /** - * The maximum length of any single component (owner, repo, name). - * 64 chars × 3 components + 2 separators + extension stays - * comfortably under the 255-byte filename limit on every - * filesystem we target. - */ private const val MAX_COMPONENT_LEN = 64 fun scoped( @@ -69,10 +16,6 @@ object AssetFileName { val safeOwner = sanitizeComponent(owner).take(MAX_COMPONENT_LEN) val safeRepo = sanitizeComponent(repo).take(MAX_COMPONENT_LEN) - // Split extension off the original name first so we don't - // mangle the dot. Extension carries semantic meaning (the - // installer dispatches on it), so we keep it intact and just - // sanitize the body. val originalLower = originalName.lowercase() val dotIndex = originalLower.lastIndexOf('.') val (body, ext) = @@ -84,10 +27,6 @@ object AssetFileName { val safeBody = sanitizeComponent(body).take(MAX_COMPONENT_LEN) val safeExt = sanitizeExtension(ext) - // Compose. Empty components are replaced with `unknown` so we - // never produce a name like `__app.apk` (the multi-underscore - // collapse would still produce `_app.apk`, which is uglier - // than carrying a placeholder). val ownerPart = safeOwner.ifBlank { "unknown" } val repoPart = safeRepo.ifBlank { "unknown" } val bodyPart = safeBody.ifBlank { "asset" } @@ -96,11 +35,6 @@ object AssetFileName { return MULTI_UNDERSCORE.replace(joined, "_") } - /** - * True when [fileName] is in the scoped format produced by [scoped]. - * Used by the orchestrator to detect "is this already namespaced" - * so callers can pass either form during the migration window. - */ fun isScoped( fileName: String, owner: String, @@ -115,26 +49,19 @@ object AssetFileName { private fun sanitizeComponent(input: String): String { if (input.isBlank()) return "" val lowered = input.lowercase() - // Replace path separators *first* so they always become a - // single `_` rather than getting eaten by FORBIDDEN's - // greedy collapse. + val withoutSeparators = lowered.replace('/', '_').replace('\\', '_') - // Strip path-traversal sequences before letter sanitization; - // a leading `..` would otherwise survive as `..` and the - // OS would interpret it. + val noDots = withoutSeparators.replace("..", "_") return FORBIDDEN.replace(noDots, "_") } private fun sanitizeExtension(ext: String): String { if (ext.isEmpty()) return "" - // Extensions are short and character-restricted by convention. - // We allow `.apk`, `.exe`, `.dmg`, `.deb`, `.rpm`, `.msi`, - // `.pkg`, `.zip` and similar — anything weirder gets the - // generic sanitizer applied. + val cleaned = FORBIDDEN.replace(ext, "") - // Re-prepend the dot if the regex stripped it. + return if (cleaned.startsWith('.')) cleaned else ".$cleaned" } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFilter.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFilter.kt index 966b3efd1..3366725e5 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFilter.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetFilter.kt @@ -1,20 +1,5 @@ package zed.rainxch.core.domain.util -/** - * Compiled, validated wrapper around a per-app asset name regex. - * - * Use [AssetFilter.parse] when reading a (possibly user-supplied) pattern out - * of storage or a form field — it returns `null` for blank input and a - * [Result.failure] for an invalid regex, so the caller can decide whether to - * surface a validation error. - * - * Once compiled, [matches] is allocation-free for the hot path used by - * `checkForUpdates` (compile once per app, evaluate against many asset names). - * - * Matching uses [Regex.containsMatchIn], not [Regex.matches]. That makes - * casual patterns like `ente-auth` or `arm64` "just work" without forcing the - * user to wrap the value in `.*` — it matches Obtainium's behaviour. - */ class AssetFilter private constructor( val pattern: String, private val regex: Regex, @@ -28,13 +13,7 @@ class AssetFilter private constructor( override fun toString(): String = "AssetFilter($pattern)" companion object { - /** - * Parses a raw user-supplied pattern. - * - * @return `null` if [raw] is null/blank, otherwise a [Result] wrapping - * either the compiled filter or the [PatternSyntaxException]-equivalent - * exception thrown by Kotlin's regex compiler. - */ + fun parse(raw: String?): Result? { val trimmed = raw?.trim().orEmpty() if (trimmed.isEmpty()) return null @@ -43,30 +22,12 @@ class AssetFilter private constructor( } } - /** - * Suggests a sensible filter regex from a sample asset name. - * Strips the version suffix (anything from the first `-` - * onward) and returns the leading prefix as a **literal-prefix - * regex** — escaped and anchored to the start of the filename so - * metacharacters in the prefix don't get interpreted as regex - * operators. - * - * Examples: - * ente-auth-3.2.5-arm64-v8a.apk → ^\Qente-auth-\E - * Photos-1.7.0-universal.apk → ^\QPhotos-\E - * app+1.2.3.apk → ^\Qapp+\E (the `+` is escaped) - * no-version.apk → null (no clear version anchor) - * - * Returns `null` when the asset name has no clear version anchor — - * blindly returning the full filename would create a filter that - * matches only that exact build. - */ fun suggestFromAssetName(assetName: String): String? { - // Try the common "name-1.2.3" / "name_1.2.3" / "name 1.2.3" patterns. + val versionAnchor = Regex("[-_ .]\\d") val match = versionAnchor.find(assetName) ?: return null val prefix = assetName.substring(0, match.range.first + 1) - // Need at least 2 meaningful chars; otherwise the suggestion is noise. + if (prefix.length < 2) return null return "^" + Regex.escape(prefix) } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetPlatform.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetPlatform.kt index 7c766d1a0..f466035fa 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetPlatform.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetPlatform.kt @@ -2,13 +2,6 @@ package zed.rainxch.core.domain.util import zed.rainxch.core.domain.model.DiscoveryPlatform -/** - * Maps a release asset filename to the OS platform it targets, by - * extension. Returns `null` for files we can't classify (zip bundles, - * sources, sig/sha files, etc.) — callers should drop those from the - * cross-platform picker so the list isn't polluted by non-installable - * sidecars. - */ fun assetPlatformOf(assetName: String): DiscoveryPlatform? { val lower = assetName.lowercase() return when { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetVariant.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetVariant.kt index 5e452959f..bb58963bf 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetVariant.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/AssetVariant.kt @@ -2,109 +2,38 @@ package zed.rainxch.core.domain.util import zed.rainxch.core.domain.model.GithubAsset -/** - * Identifies the "variant" of a GitHub release asset — the part of the - * filename that stays stable across releases (architecture, packaging - * flavour, etc.) so the user's choice can survive a version bump. - * - * # Why this is non-trivial - * - * Release filenames are wildly inconsistent. The naïve approach - * (substring after the version segment) breaks on a handful of common - * shapes: - * - * - **Arch-before-version**: `app-arm64-v8a-1.2.3.apk` — version is at - * the end, the variant token is in front of it - * - **Counter between version and arch**: `app-1.2.3-beta.1-arm64-v8a.apk` - * — substring tail would include `beta.1`, drifting release-over-release - * - **Version-code-only naming**: `app_1234_arm64-v8a.apk` — no dotted - * version anchor - * - **OS-version interlopers**: `app-android-12.0-1.2.3-arm64.apk` — the - * OS version is the first dotted-digit substring - * - * # The strategy - * - * Three layers of identity, each cheaper to compute than the next, all - * consulted when matching against a fresh release: - * - * 1. **Token-set fingerprint** ([extractTokens]): pulls a *set* of - * well-known arch / flavor / qualifier tokens out of the filename - * regardless of position. Two filenames are "the same variant" if - * their token sets are equal. This is the primary identity and - * handles arch-before-version, OS-version interlopers, and counters - * in one go. - * - * 2. **Glob-pattern fingerprint** ([deriveGlob]): replaces version-shaped - * substrings with `*` (and digit-only run substrings with `*`), - * yielding e.g. `app-*-arm64-v8a.apk` from `app-1.2.3-arm64-v8a.apk`. - * Used as a secondary key when the token vocabulary doesn't recognise - * anything in the filename — covers custom flavor names like - * `myapp-foss-1.2.3.apk` → `myapp-foss-*.apk`. - * - * 3. **Substring-tail extract** ([extract]): the legacy fallback. Kept - * for surface compatibility with the existing UI (badge labels) and - * because it's a cheap way to display *something* readable to the - * user when the token vocabulary returns nothing. - * - * # Vocabulary policy - * - * The token vocabulary is intentionally a closed set. Open-ended token - * extraction (e.g. "anything that's not a digit run") leaks app names, - * release qualifiers, and date components into the variant identity, - * which is exactly what the substring-tail approach gets wrong. The - * vocabulary covers the tokens that actually distinguish APK variants - * in the wild — architectures, install flavors, signing qualifiers. - * - * Casing is normalised to lowercase before comparison; some maintainers - * flip casing release-over-release. - */ object AssetVariant { - /** - * Closed vocabulary of tokens that are meaningful for variant - * identity. Anything *not* in this set is considered noise (app name, - * release qualifier, date component, etc.) and is ignored. - * - * Architecture tokens dominate the list because that's the most - * common variant axis on Android. Flavor tokens (`fdroid`, `play`, - * `foss`, `gms`, `nogms`, `huawei`) cover the second-most-common - * axis: same arch, different distribution channel. - * - * `release` and `signed` are explicitly excluded — they appear in - * a lot of filenames but rarely *distinguish* assets within a - * single release. Including them would create false negatives - * when one release uses `release-arm64-v8a` and the next uses - * `arm64-v8a`. Same reasoning applies to `aligned`, `unsigned`. - */ + private val ARCH_TOKENS = setOf( - // 64-bit ARM (the modern default on Android) + "arm64-v8a", "arm64", "aarch64", - // 32-bit ARM + "armeabi-v7a", "armeabi", "armv7", "armv7a", "armv8", - // x86 family + "x86_64", "x86-64", "x64", "x86", "i386", "i686", - // MIPS (rare but real on legacy hardware) + "mips", "mips64", - // Universal / fat APKs + "universal", "all", ) private val FLAVOR_TOKENS = setOf( - // Distribution channels + "fdroid", "f-droid", "play", @@ -115,7 +44,7 @@ object AssetVariant { "huawei", "amazon", "samsung", - // Build flavors + "foss", "libre", "free", @@ -129,31 +58,11 @@ object AssetVariant { "nightly", ) - /** - * The full vocabulary used for token-set comparison. Lazily merged - * because the JVM `Set` union allocates and there's no reason to do - * it on every call. - */ private val VOCABULARY: Set by lazy { ARCH_TOKENS + FLAVOR_TOKENS } - /** - * Strips hyphens/underscores so alias spellings of the same flavor - * (`f-droid` ⇄ `fdroid`, `play-store` ⇄ `playstore`) collapse to a - * single comparable form. Used by [filterByPackageFlavor] to align - * asset-token output (which may carry hyphenated bi-grams) with - * package-segment input (which can't legally contain hyphens). - */ private fun canonicalFlavorToken(token: String): String = token.replace('-', ' ').replace('_', ' ').replace(" ", "").lowercase() - /** - * Splits a filename into candidate tokens. Splits on the usual - * separators (`-`, `_`, ` `, `.`) so that compound names like - * `arm64-v8a` survive intact only after the recombine step below. - * - * Note: tokens are *normalised to lowercase* and the file extension - * is stripped first. - */ private fun tokenize(assetName: String): List { val withoutExt = assetName.substringBeforeLast('.') return withoutExt @@ -162,34 +71,14 @@ object AssetVariant { .filter { it.isNotEmpty() } } - /** - * Extracts the **token-set fingerprint** of [assetName]. Returns the - * subset of [VOCABULARY] that appears in the filename, accounting for - * compound tokens that span the splitter (e.g. `arm64-v8a` is - * tokenised as `["arm64", "v8a"]` but recognised as the compound - * `arm64-v8a` via a sliding-window check). - * - * Two assets share the same variant identity iff [extractTokens] - * returns equal sets. - * - * Returns an empty set when the filename contains no recognisable - * tokens — that's a deliberate "no token-based identity available, - * fall through to the next layer" signal. - */ fun extractTokens(assetName: String): Set { val tokens = tokenize(assetName) if (tokens.isEmpty()) return emptySet() val found = mutableSetOf() - // First pass: 3-grams, 2-grams, then 1-grams. Order matters - // because longer matches should take precedence: matching - // `armeabi-v7a` should not also match the bare `armeabi` - // afterwards (they're the same conceptual variant in different - // tokenisations of the maintainer's filename). val consumed = BooleanArray(tokens.size) - // 3-grams (rare but `armeabi-v7a-release` style exists) for (i in 0 until tokens.size - 2) { if (consumed[i] || consumed[i + 1] || consumed[i + 2]) continue val candidate = "${tokens[i]}-${tokens[i + 1]}-${tokens[i + 2]}" @@ -201,11 +90,6 @@ object AssetVariant { } } - // 2-grams: `arm64-v8a`, `armeabi-v7a`, `x86-64`, `x86_64` - // The tokenizer split on both `-` and `_`, so `x86_64` becomes - // `["x86", "64"]`; we recombine with `-` for vocabulary lookup - // *and* try the underscore form to cover `x86_64`. Both forms - // appear in the wild from different maintainers. for (i in 0 until tokens.size - 1) { if (consumed[i] || consumed[i + 1]) continue val dashed = "${tokens[i]}-${tokens[i + 1]}" @@ -223,7 +107,6 @@ object AssetVariant { } } - // 1-grams: bare tokens for (i in tokens.indices) { if (consumed[i]) continue if (tokens[i] in VOCABULARY) { @@ -235,57 +118,10 @@ object AssetVariant { return found } - /** - * Derives a **glob-pattern fingerprint** from [assetName]: replaces - * any dotted-digit version segment and any standalone digit run with - * `*`, leaving everything else intact. Used as a secondary identity - * when [extractTokens] returns an empty set. - * - * Examples: - * - * `app-1.2.3-arm64-v8a.apk` → `app-*-arm64-v8a.apk` - * `myapp-foss-1.2.3.apk` → `myapp-foss-*.apk` - * `Project_v2.0.1_universal.apk` → `project_v*_universal.apk` - * `release-2024.04.10-debug.apk` → `release-*-debug.apk` - * `app_1234_arm64.apk` → `app_*_arm64.apk` - * - * The result is also lowercased so two maintainers with different - * casing conventions still produce the same fingerprint. - * - * Returns `null` when the filename has no version-shaped or - * digit-run substring at all — there'd be nothing to wildcard, so - * the glob would just equal the filename and provides no rescue - * value beyond exact-match. - */ - /** - * Extracts the **base-name stem** of an asset — the lowercased, - * separator-stripped concatenation of every token that isn't a - * version-like number, an arch token, or a flavor token. Used to - * detect "sibling app in the same repo" cases where two releases - * ship `AppA-1.10.apk` and `AppB-2.20.apk` and the auto-picker - * would otherwise swap one for the other based on numeric version - * alone (issue #591). - * - * `AppA-1.10.apk` → `"appa"` - * `AppB-2.20.apk` → `"appb"` - * `app-arm64-v8a-1.10.apk` and `app-x86_64-1.10.apk` → both `"app"` - * `app-1.0.apk` and `app-fdroid-1.0.apk` → both `"app"` - * - * Returns an empty string when stripping leaves nothing behind - * (release ships only a versioned filename like `2.0.apk`). Callers - * treat empty as "no stem signal — don't filter". - */ fun extractBaseStem(assetName: String): String { val tokens = tokenize(assetName) if (tokens.isEmpty()) return "" - // Mirror `extractTokens`' n-gram consumption pass so the - // fragments of compound vocab entries (e.g. `arm64-v8a` → - // tokens `["arm64","v8a"]`) are both stripped, not just the - // canonical-form half. Without this, `v8a` / `v7a` would - // survive the filter and `app-arm64-v8a-1.10.apk` would yield - // a different stem than `app-x86_64-1.10.apk`, defeating the - // sibling-app detection for arch-variant releases. val consumed = BooleanArray(tokens.size) for (i in 0 until tokens.size - 2) { @@ -315,13 +151,6 @@ object AssetVariant { return out.toString() } - /** - * `1`, `10`, `1.0.0`, `v2.0.1`, `2024.04.10`, `1.0-rc1`, `beta3` — - * common patterns used in release filenames to encode the version. - * Conservative on purpose: false positives here just lose a stem - * character; false negatives would let a numeric variant leak into - * the stem and break the sibling-app detection. - */ private fun isVersionLikeToken(token: String): Boolean { if (token.isEmpty()) return false if (token.all { it.isDigit() }) return true @@ -331,33 +160,13 @@ object AssetVariant { fun deriveGlob(assetName: String): String? { val lower = assetName.lowercase() - // Match either: - // - a versioned segment with at least one dot (e.g. `1.2.3`, - // `v2.0.1`, `2024.04.10`) - // - OR a standalone digit run of length >= 2 (the `1234` - // version-code-only case). Length >= 2 avoids replacing - // legitimate single-digit tokens like `v8` in `arm64-v8a`. + val versionPattern = Regex("""v?\d+(?:\.\d+)+|(?, pinnedVariant: String?, pinnedTokens: Set? = null, pinnedGlob: String? = null, ): GithubAsset? { - // Layer 1: token-set match. The strongest signal — survives - // arch-before-version, OS-version interlopers, and counters. + if (!pinnedTokens.isNullOrEmpty()) { val match = assets.firstOrNull { asset -> extractTokens(asset.name) == pinnedTokens @@ -430,8 +210,6 @@ object AssetVariant { if (match != null) return match } - // Layer 2: glob match. Catches custom flavor names that the - // closed token vocabulary doesn't know about. if (!pinnedGlob.isNullOrBlank()) { val match = assets.firstOrNull { asset -> deriveGlob(asset.name) == pinnedGlob @@ -439,26 +217,12 @@ object AssetVariant { if (match != null) return match } - // Layer 3: legacy tail-string match. Keeps rows pinned before - // the multi-layer rewrite working without forcing a re-pick. val target = pinnedVariant?.trim()?.takeIf { it.isNotBlank() } ?: return null return assets.firstOrNull { asset -> extract(asset.name)?.equals(target, ignoreCase = true) == true } } - /** - * Same-position fallback used by the resolver as a last resort - * before falling back to the platform auto-picker. When the new - * release contains exactly the same number of installable assets - * as the original picked-from release, returning the asset at the - * same index preserves the user's intent in cases where every - * asset has been renamed in lockstep (e.g. an entire flavor - * dimension was added). - * - * Returns `null` when [siblingCountAtPickTime] is null, zero, - * or doesn't match `assets.size`. - */ fun resolveBySamePosition( assets: List, originalIndex: Int?, @@ -470,20 +234,6 @@ object AssetVariant { return assets.getOrNull(originalIndex) } - /** - * Pulls the variant tag out of a sample asset filename and returns - * it normalised, or `null` when the name doesn't carry a meaningful - * variant. Skips the work entirely when [siblingAssetCount] is 1 or 0 - * because single-asset releases have nothing to remember. - * - * The returned tail is the **display label** that gets stored in - * `InstalledApp.preferredAssetVariant` and shown to the user. The - * matching algorithm itself uses [extractTokens] / [deriveGlob] — - * those are also derived at pin time and stored separately. - * - * Single-asset releases and "no variant suffix" filenames both return - * `null` rather than the empty string — there's nothing to pin. - */ fun deriveFromPickedAsset( pickedAssetName: String, siblingAssetCount: Int, @@ -493,22 +243,6 @@ object AssetVariant { return variant.takeIf { it.isNotEmpty() } } - /** - * Bundle of all three identity layers derived from a single picked - * asset filename. Used by the persistence path so the caller can - * write all fingerprints atomically and the resolver can match on - * any of them later. - * - * - [variant]: legacy substring tail (display label) - * - [tokens]: token-set fingerprint, empty when the filename has - * no vocabulary tokens - * - [glob]: glob-pattern fingerprint, null when there's no - * version-shaped substring to wildcard - * - * Returns `null` when [siblingAssetCount] <= 1 (nothing to pin) or - * when *all three* identity layers came up empty — at that point - * the asset has no stable fingerprint and pinning would be a lie. - */ data class VariantFingerprint( val variant: String?, val tokens: Set, @@ -523,23 +257,11 @@ object AssetVariant { val variant = extract(pickedAssetName)?.takeIf { it.isNotEmpty() } val tokens = extractTokens(pickedAssetName) val glob = deriveGlob(pickedAssetName) - // If everything is empty there's nothing to remember — return - // null so callers persist `null` and fall back to the platform - // auto-picker on update. + if (variant == null && tokens.isEmpty() && glob == null) return null return VariantFingerprint(variant = variant, tokens = tokens, glob = glob) } - /** - * Serializes a token set to a stable string for storage. Sorted so - * that identical sets always serialize to identical strings, which - * is what makes string equality a valid set-equality check at the - * SQL layer (avoiding a JSON column or a join table). - * - * Format: tokens joined by `|`. Returns `null` for empty sets so - * the column can stay nullable and the resolver knows when there's - * no token fingerprint to compare against. - */ fun serializeTokens(tokens: Set): String? { if (tokens.isEmpty()) return null return tokens.sorted().joinToString("|") @@ -550,31 +272,6 @@ object AssetVariant { return serialized.split('|').filter { it.isNotBlank() }.toSet() } - /** - * Narrows [assets] to the subset whose filename flavor matches the - * flavor implied by [trackedPackageName]. Used by the update-check - * auto-picker to avoid swapping the user's installed APK for a - * sibling release artifact that ships a different package id - * (typically a `.fdroid` variant alongside the stock package). - * - * The picker only knows asset names — the actual APK package is - * inside the binary, which would require downloading every - * candidate. Filenames are usually honest about the flavor though: - * an `.apk` named `app-fdroid-release.apk` almost always installs - * with package id `.fdroid`. We exploit that to keep the - * auto-pick safe by default. - * - * Rules: - * - If the tracked package contains any [FLAVOR_TOKENS] segment - * (case-insensitive, dot-separated), keep only assets whose - * filename tokens contain at least one of those flavor tokens. - * - Otherwise (stock package, no flavor marker), keep only assets - * whose filename tokens contain NO flavor markers. - * - If either filter would eliminate every candidate the input is - * returned unchanged — losing the auto-update prompt is worse - * than picking a marginally wrong-flavor asset, and the user - * can still pin the right variant from the picker UI. - */ fun filterByPackageFlavor( assets: List, trackedPackageName: String, @@ -582,12 +279,7 @@ object AssetVariant { if (assets.isEmpty()) return assets val packageSegments = trackedPackageName.lowercase().split('.').filter { it.isNotBlank() } - // Canonicalise both sides via hyphen/underscore strip so package - // segments (`fdroid`, can't legally hold `-`) match against asset - // tokens that may come back hyphenated (`f-droid`) when the - // filename uses an aliased spelling. Without this, the set - // intersection misses the alias forms `FLAVOR_TOKENS` deliberately - // covers. + val packageFlavorTokens = packageSegments.filter { it in FLAVOR_TOKENS } .map(::canonicalFlavorToken) .toSet() diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/EmojiShortcodes.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/EmojiShortcodes.kt index 531472c50..cd7f8e37d 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/EmojiShortcodes.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/EmojiShortcodes.kt @@ -1,14 +1,10 @@ package zed.rainxch.core.domain.util object EmojiShortcodes { - // Subset of github/gemoji that covers the long tail of READMEs. - // ~250 entries — picked from gemoji download stats + common usage in - // README/CHANGELOG headers. Not exhaustive on purpose; missing - // shortcodes survive as literal text rather than ballooning the - // bundle with a 1500-entry table we mostly don't need. + private val TABLE: Map = mapOf( - // Frequently used in README hero / status sections + "rocket" to "🚀", "sparkles" to "✨", "tada" to "🎉", @@ -36,7 +32,7 @@ object EmojiShortcodes { "point_left" to "👈", "point_up" to "☝️", "point_down" to "👇", - // Status / signals + "warning" to "⚠️", "white_check_mark" to "✅", "heavy_check_mark" to "✔️", @@ -54,7 +50,7 @@ object EmojiShortcodes { "grey_question" to "❔", "grey_exclamation" to "❕", "information_source" to "ℹ️", - // Hearts + "heart" to "❤️", "yellow_heart" to "💛", "green_heart" to "💚", @@ -67,7 +63,7 @@ object EmojiShortcodes { "broken_heart" to "💔", "heart_eyes" to "😍", "hearts" to "♥️", - // Faces + "smile" to "😄", "smiley" to "😃", "grin" to "😁", @@ -106,7 +102,7 @@ object EmojiShortcodes { "poop" to "💩", "hankey" to "💩", "shit" to "💩", - // Dev / engineer + "computer" to "💻", "iphone" to "📱", "watch" to "⌚", @@ -140,7 +136,7 @@ object EmojiShortcodes { "mute" to "🔇", "mega" to "📣", "loudspeaker" to "📢", - // Tools / build + "hammer" to "🔨", "wrench" to "🔧", "screwdriver" to "🪛", @@ -159,7 +155,7 @@ object EmojiShortcodes { "magnet" to "🧲", "balance_scale" to "⚖️", "scales" to "⚖️", - // Symbols + "bug" to "🐛", "ant" to "🐜", "bulb" to "💡", @@ -208,7 +204,7 @@ object EmojiShortcodes { "earth_africa" to "🌍", "earth_asia" to "🌏", "globe_with_meridians" to "🌐", - // Animals frequently in READMEs + "octocat" to "🐙", "cat" to "🐱", "dog" to "🐶", @@ -234,7 +230,7 @@ object EmojiShortcodes { "see_no_evil" to "🙈", "hear_no_evil" to "🙉", "speak_no_evil" to "🙊", - // Food / coffee + "coffee" to "☕", "tea" to "🍵", "beer" to "🍺", @@ -299,7 +295,7 @@ object EmojiShortcodes { "wilted_flower" to "🥀", "bouquet" to "💐", "cherry_blossom" to "🌸", - // Travel / weather + "airplane" to "✈️", "rocket_ship" to "🚀", "car" to "🚗", @@ -330,10 +326,6 @@ object EmojiShortcodes { "tornado" to "🌪️", ) - // [a-z0-9_+\-] is a superset of github shortcode chars. The - // bookend `:` markers must be tight against the token (no spaces), - // and the shortcode itself must not embed `:` (so URL like - // `https://example.com:8080` doesn't grab `8080` as a code). private val PATTERN = Regex(""":([a-z0-9_+\-]{1,40}):""") fun render(input: String): String { @@ -350,11 +342,6 @@ object EmojiShortcodes { TABLE[key] ?: match.value } - // Split the markdown body into alternating (text, code) regions - // anchored on triple-backtick fences. Single-backtick inline code - // is left in the text regions on purpose — losing rare emoji - // collisions in inline code is fine; properly tokenising inline - // spans isn't worth the parser complexity. private data class Chunk(val content: String, val isCode: Boolean) private fun splitOutCodeRegions(input: String): List { @@ -369,10 +356,7 @@ object EmojiShortcodes { out += Chunk(input.substring(i), isCode = inCode) break } - // Take everything up to and including the fence into the - // current region — the fence itself is part of the code - // block when we're transitioning out, so always include - // 3 chars in the "code" side. + if (inCode) { out += Chunk(input.substring(i, next + fence.length), isCode = true) } else { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/ExternalInstallVerdict.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/ExternalInstallVerdict.kt index fcaa45f87..4a983431a 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/ExternalInstallVerdict.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/ExternalInstallVerdict.kt @@ -2,65 +2,21 @@ package zed.rainxch.core.domain.util import zed.rainxch.core.domain.model.InstalledApp -/** - * Decision produced by [resolveExternalInstallVerdict] after an - * externally-performed install/update surfaces via PackageManager. - */ enum class VersionVerdict { - /** System version meets or exceeds every signal we have. */ + UP_TO_DATE, - /** System version is strictly older than at least one reliable signal. */ UPDATE_AVAILABLE, - /** - * No signal was reliable enough to decide. The caller should - * leave the current `isUpdateAvailable` flag alone and trigger - * an authoritative re-check via - * [zed.rainxch.core.domain.repository.InstalledAppsRepository.checkForUpdates]. - */ UNKNOWN, } -/** - * Best-effort local verdict after an external install/update. - * - * An Android APK install gives us two pieces of truth — [newVersionName] - * and [newVersionCode] — both of which are stricter than what we can - * usually know about the tracked "latest" release. We try a ladder of - * signals, stopping at the first one that can produce a reliable answer: - * - * 1. **versionCode comparison.** Monotonic integer by Android contract. - * Only usable when we've captured a non-zero `latestVersionCode` for - * this app (i.e. a previous install round stamped it). If both sides - * have a real `versionCode`, one comparison nails the answer. - * - * 2. **versionName vs latestVersionName.** These are the post-install - * values from PackageManager, which means same axis. Run through - * [VersionMath.normalizeVersion] so `1.2.3` and `v1.2.3-stable` line - * up, then semver-compare. - * - * 3. **versionName vs latestVersion (release tag).** Works when the - * maintainer's tag contains the real version (e.g. `v1.2.3`, - * `release-1.2.3`, `build-2025.04.10`) — [VersionMath.normalizeVersion] - * extracts the dotted-digit core. - * - * 4. **Give up.** Return [VersionVerdict.UNKNOWN]. The caller is - * expected to defer to the network-backed - * [zed.rainxch.core.domain.repository.InstalledAppsRepository.checkForUpdates], - * which does the same comparison with fresh GitHub release data. - * - * This function is pure — it does not read or write any state, and it - * makes no network or disk calls. Callers combine it with an async - * authoritative re-check so that an incorrect optimistic answer is - * corrected within seconds. - */ fun resolveExternalInstallVerdict( app: InstalledApp, newVersionName: String, newVersionCode: Long, ): VersionVerdict { - // Priority 1: integer versionCode (most reliable when available). + val latestVersionCode = app.latestVersionCode ?: 0L if (latestVersionCode > 0L && newVersionCode > 0L) { return if (newVersionCode >= latestVersionCode) { @@ -70,17 +26,12 @@ fun resolveExternalInstallVerdict( } } - // Priority 2: versionName ↔ latestVersionName (same axis — both - // come from PackageManager on their respective installs). val latestName = app.latestVersionName if (!latestName.isNullOrBlank() && newVersionName.isNotBlank()) { val verdict = compareAndDecide(newVersionName, latestName) if (verdict != VersionVerdict.UNKNOWN) return verdict } - // Priority 3: versionName ↔ latestVersion (release tag). Different - // axis but VersionMath.normalizeVersion handles the common cases - // (`v1.2.3`, `release-1.2.0`, `App-v1.2.0-stable`, …). val latestTag = app.latestVersion if (!latestTag.isNullOrBlank() && newVersionName.isNotBlank()) { val verdict = compareAndDecide(newVersionName, latestTag) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepoIdCodec.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepoIdCodec.kt index ab05cf578..69766db11 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepoIdCodec.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepoIdCodec.kt @@ -1,36 +1,8 @@ package zed.rainxch.core.domain.util -/** - * Packs a foreign-host repo identity into the `Long` `repoId` slot - * used throughout the schema (which was originally GitHub-only, where - * IDs are globally unique). - * - * Layout (64-bit `Long`): - * - bit 63 — sign flag. Negative means "foreign source" (see - * [isForeignSource]); positive means native GitHub. - * - bits 40-62 — 23-bit host fingerprint, derived from - * `host.lowercase().hashCode()`. 23 bits gives 8 388 608 - * buckets — birthday-collision probability for a user - * tracking N self-hosted forges is roughly - * `N² / (2 × 8 388 608)`; e.g. 200 forges → ~0.0025 - * chance of any two sharing a bucket. The previous 15-bit - * layout (32 768 buckets) hit ~0.6 probability at that - * scale, which CR flagged as a real risk. - * - bits 0-39 — 40-bit raw repository ID. Forgejo / Gitea instances - * auto-increment from 1; 2⁴⁰ ≈ 1 trillion comfortably - * covers any realistic instance. - * - * Note: this layout is NOT backwards-compatible with the original - * 15-bit fingerprint encoding. Database rows persisted under the old - * scheme will decode to different `repoId`s after upgrade. Because the - * Codeberg / Forgejo source feature is shipping in a preview release - * (versionCode 18 — see whatsnew/18.json), the migration cost is - * accepted; existing rows for non-GitHub sources will simply be - * re-fetched on next scan. - */ object RepoIdCodec { - private const val HOST_FINGERPRINT_MASK = 0x7FFFFFL // 23 bits - private const val RAW_ID_MASK = 0xFFFFFFFFFFL // 40 bits + private const val HOST_FINGERPRINT_MASK = 0x7FFFFFL + private const val RAW_ID_MASK = 0xFFFFFFFFFFL private const val HOST_FINGERPRINT_SHIFT = 40 fun encode(host: String?, rawId: Long): Long { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepositoryUrlParser.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepositoryUrlParser.kt index 80ff1e473..852b01af8 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepositoryUrlParser.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/RepositoryUrlParser.kt @@ -44,14 +44,7 @@ object RepositoryUrlParser { private fun looksLikeForgejoHost(host: String): Boolean { val lower = host.lowercase() - // Conservative heuristic: only match hosts whose name literally - // contains a forge brand or starts with `git.` (the de-facto - // self-hosted convention). Previously `code.*` and `source.*` - // were also matched and produced false positives for - // `code.visualstudio.com`, `source.android.com`, - // `source.unsplash.com`, and similar non-forge services. Users - // with idiosyncratic hostnames should add them via - // Tweaks → Custom forges instead. + return lower.contains("forgejo") || lower.contains("gitea") || lower.startsWith("git.") diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/SeparateAdjacentImageLinks.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/SeparateAdjacentImageLinks.kt index b36ffba9d..8a619fe68 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/SeparateAdjacentImageLinks.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/SeparateAdjacentImageLinks.kt @@ -1,33 +1,5 @@ package zed.rainxch.core.domain.util -/** - * Splits adjacent badge / image-link lines into separate paragraphs so - * each renders as a block-level element instead of inline content in a - * single paragraph. - * - * Why this matters: markdown like - * - * ``` - * [![Apache-2.0](badge1.svg)](url1) - * [![Downloads](badge2.svg)](url2) - * [![Stars](badge3.svg)](url3) - * ``` - * - * is one paragraph with soft line breaks. The renderer (mikepenz lib) - * uses ONE shared `Placeholder` for all inline images in a paragraph - * — when image height > the placeholder slot, adjacent images paint - * into each other's vertical space and visually overlap. Splitting - * the lines with blank rows turns each link into its own paragraph, - * which the lib renders via the block-level image component - * (`LinkAwareMarkdownImage` in our case) where size is controlled - * cleanly. - * - * Strategy: walk line by line. If a line contains ONLY a single - * markdown link / image-link / HTML anchor-wrapping-image (i.e. it - * is "structurally a badge row"), and the PREVIOUS non-blank line - * also contained ONLY a link / image, ensure there's a blank row - * between them. No-op for everything else. - */ fun separateAdjacentImageLinks(content: String): String { if (content.isEmpty()) return content val lines = content.split('\n') @@ -51,38 +23,27 @@ fun separateAdjacentImageLinks(content: String): String { return out.toString() } -/** - * `true` when [line] (already trimmed) is composed purely of image-link - * structures with optional whitespace between — i.e. nothing else but - * markdown link / image-link / HTML `......` constructs. - * - * Heuristic-quality only. Aim is to recognise common badge rows in the - * wild without misclassifying paragraphs that happen to start with an - * image link inline next to prose. - */ private fun isPureImageLinkLine(line: String): Boolean { - // Cheap rejection: must contain `](` (markdown link) or ` ... ` blocks that contain an `` + stripped = stripped.replace( Regex( """]*>\s*(?:]*/?>|\s)*""", @@ -90,7 +51,7 @@ private fun isPureImageLinkLine(line: String): Boolean { ), "", ) - // Standalone `` + stripped = stripped.replace( Regex("""]*/?>""", RegexOption.IGNORE_CASE), "", diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt index 69d1b7f26..70e7fdf4a 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/VersionMath.kt @@ -1,74 +1,16 @@ package zed.rainxch.core.domain.util -/** - * Single source of truth for version-string normalization and ordering - * across the app. Both the periodic update check - * (`InstalledAppsRepositoryImpl.checkForUpdates`) and the external-install - * detection path (`ExternalInstallVerdict`) now call through here so a - * single comparator change propagates everywhere instead of drifting - * between private copies. - * - * Design invariants: - * - Every public function is pure; no I/O, no time, no randomness. - * - Inputs are `String?` where realistic so callers don't have to - * guard against nulls from the DB or the release feed. - * - Semver-compatible strings get semver semantics (including - * `-preRelease` ordering per spec: `1.0.0-beta < 1.0.0`). - * - Non-semver strings degrade gracefully: we try to extract a - * dotted-digit core (so `release-1.2.3` still compares like - * `1.2.3`), and only fall back to lexicographic comparison when - * the string has no recognisable version core at all. - */ object VersionMath { - /** - * Reduces a tag or installed-version string to a form that - * [parseSemanticVersion] can digest. - * - * Strategy, in order: - * 1. Trim and strip common tag prefixes (`refs/tags/`, `v`, `V`). - * 2. Drop `+build` metadata (per semver spec, ignored for - * ordering). - * 3. If the result parses as semver, return it. - * 4. Otherwise extract the first dotted-digit substring - * (optionally followed by a `-pre` identifier) and return - * that — handles maintainer prefixes like `release-1.2.0`, - * `App-v1.2.0-stable`, `build-2025.04.10`. - * 5. If nothing numeric is found at all, return the cleaned - * string so the caller can fall back to equality / lex. - * - * Examples: - * `v1.2.3` → `1.2.3` - * `1.2.3+sha.abcd` → `1.2.3` - * `1.2.3-rc1` → `1.2.3-rc1` - * `release-1.2.0` → `1.2.0` - * `App-v1.2.0-stable` → `1.2.0-stable` - * `build-2025.04.10` → `2025.04.10` - * `refs/tags/v1.2.3` → `1.2.3` - * `not-a-version` → `not-a-version` - * `null` → `""` - */ + fun normalizeVersion(version: String?): String { if (version.isNullOrBlank()) return "" val cleaned = stripFullPrefix(version) val withoutBuildMetadata = cleaned.substringBefore('+') - // Hyphenated calver (`2024-10-15`) — semver would otherwise read - // the trailing `-10-15` as a pre-release identifier and rank - // `2024-10-15 < 2024`, which is the opposite of what users mean - // by a date-stamped release. Re-shape into dotted calver so the - // semver path numeric-compares each segment. + val calverNormalized = normalizeCalverHyphen(withoutBuildMetadata) - // Adjacent-letter pre-release (`1.2.0beta01`) — insert the - // missing hyphen so the numeric core and the marker can be - // parsed independently. Only inserts before known markers to - // avoid mauling architecture suffixes like `1arm64`. + val separated = insertHyphenBeforeKnownMarker(calverNormalized) - // Build-variant suffix (`-f`, `-m`, `-arm64`, `-stable`, …) — - // some maintainers append a flavour marker to the installed - // versionName but tag GitHub with the bare semver core, so the - // exact same artefact reads as `1.8.6-f` on-device but `1.8.6` - // in the release feed. Strip these flavour markers so the - // comparator doesn't perpetually report a phantom update. - // Pre-release markers (`-beta`, `-rc1`, …) are preserved. + val deflavoured = stripBuildVariantSuffix(separated) if (parseSemanticVersion(deflavoured) != null) { return deflavoured @@ -83,10 +25,7 @@ object VersionMath { .trim() .removePrefix("refs/tags/") .trim() - // Word-style prefixes like `version-`, `release/`, `app_`, - // `build-`, `ver.` — common in maintainer-customised tag - // schemes. Strips a single leading word and any single - // separator before the version core. Case-insensitive. + val wordMatch = VERSION_WORD_PREFIX.find(trimmed) val withoutWord = if (wordMatch != null) trimmed.substring(wordMatch.range.last + 1) else trimmed return withoutWord.removePrefix("v").removePrefix("V").trim() @@ -102,40 +41,6 @@ object VersionMath { return if (tail.isNotEmpty()) "$core-$tail" else core } - /** - * Strip a curated set of build-variant / flavour markers that - * routinely appear in installed `versionName`s but never in the - * GitHub release tag (the maintainer ships a single `1.8.6` tag - * but emits `1.8.6-f`, `1.8.6-m` APKs). Without this step the - * comparator reads the suffix as a semver pre-release identifier, - * ranks the bare tag higher, and surfaces a phantom update. - * - * Recognised markers (case-insensitive, before any `.` separator - * inside the pre-release segment): - * - **Build flavour**: `f` / `full`, `m` / `mini` / `minified`, - * `l` / `lite`, `r` / `release`, `d` / `debug`, `x` / `extended`. - * - **Channel**: `stable`, `final`, `prod`, `production`, `gms`, - * `fdroid`, `github`, `store`. - * - **Architecture**: `armv7`, `armv8`, `arm64`, `armeabi`, `x86`, - * `x64`, `x86_64`, `universal`, `android`, `ios`. - * - * Intentionally NOT stripped (these are real pre-release markers): - * - `alpha`, `beta`, `rc`, `preview`, `prerelease`, `snapshot`, - * `canary`, `nightly`, `milestone`, `ea`, `dev`, `pre`, `m\d+`. - * - * The function only acts when [version] parses as semver; if the - * input lacks a recognisable numeric core there's nothing to anchor - * a "core + flavour" split to and we leave the string alone. - * - * Examples: - * `1.8.6-f` → `1.8.6` (full APK) - * `1.8.6-m` → `1.8.6` (minified APK) - * `1.8.6-arm64` → `1.8.6` (architecture) - * `1.8.6-stable` → `1.8.6` (channel) - * `1.8.6-b` → `1.8.6-b` (untouched: `b` could be beta) - * `1.8.6-beta` → `1.8.6-beta` (real pre-release) - * `1.8.6-rc.1` → `1.8.6-rc.1` (real pre-release) - */ private fun stripBuildVariantSuffix(version: String): String { val parsed = parseSemanticVersion(version) ?: return version val pre = parsed.preRelease ?: return version @@ -145,15 +50,10 @@ object VersionMath { private fun isBuildVariantMarker(preRelease: String): Boolean { if (preRelease.isEmpty()) return false - // Compound pre-release identifiers (`armv7-beta`, `rc.1`) are - // ambiguous: even if the first token looks like a build flavour, - // a downstream segment may signal a real pre-release. Bail and - // keep the suffix intact rather than risk silently swallowing - // pre-release intent. + if (preRelease.contains('.') || preRelease.contains('-')) return false val token = preRelease.lowercase() - // Real pre-release markers always win — don't strip something - // that semver/users treat as a pre-release identifier. + if (KNOWN_PRE_RELEASE_PREFIXES.any { token.startsWith(it) }) { return false } @@ -163,15 +63,15 @@ object VersionMath { private val BUILD_VARIANT_LITERALS = setOf( - // Build flavours (single-letter) + "f", "m", "l", "r", "d", "x", - // Build flavours (words) + "full", "mini", "minified", "lite", "release", "debug", "extended", - // Distribution channel + "stable", "final", "prod", "production", "gms", "fdroid", "github", "store", - // Architecture + "armv7", "armv8", "arm64", "armeabi", "x86", "x64", "x86_64", "universal", "android", "ios", @@ -181,11 +81,7 @@ object VersionMath { val match = ADJACENT_ALPHA_PATTERN.find(s) ?: return s val letterStart = match.range.first + 1 val tail = s.substring(letterStart).lowercase() - // `m\d+` covers the JetBrains-style milestone shorthand (`m5`, - // `m12`) which `PRE_RELEASE_MARKER_PATTERN` already matches but - // a string `startsWith` over [KNOWN_PRE_RELEASE_PREFIXES] does - // not — without the explicit regex check, `1.2.0m5` would slip - // past detection and silently rank as stable `1.2.0`. + val isKnownMarker = KNOWN_PRE_RELEASE_PREFIXES.any { tail.startsWith(it) } || M_DIGIT_TAIL_PATTERN.containsMatchIn(tail) @@ -193,15 +89,6 @@ object VersionMath { return s.substring(0, letterStart) + "-" + s.substring(letterStart) } - /** - * Returns `true` if [candidate] is strictly newer than [current] - * after normalization. Handles semver (including pre-release - * ordering per spec) and falls back to lexicographic comparison - * for strings with no parseable version core. - * - * Both arguments are normalized via [normalizeVersion] before - * comparison, so callers can pass raw tag strings. - */ fun isVersionNewer(candidate: String?, current: String?): Boolean { val normCandidate = normalizeVersion(candidate) val normCurrent = normalizeVersion(current) @@ -210,55 +97,14 @@ object VersionMath { return compareNormalized(normCandidate, normCurrent) > 0 } - /** - * Three-way comparison of two raw version strings after - * normalization. Returns a positive int if [a] > [b], negative if - * [a] < [b], `0` if equal or both empty. - * - * Use this when you need the full ordering (e.g. detecting - * downgrades). Prefer [isVersionNewer] when you just need a - * boolean. - */ fun compareVersions(a: String?, b: String?): Int { val normA = normalizeVersion(a) val normB = normalizeVersion(b) return compareNormalized(normA, normB) } - /** - * Returns `true` when both inputs normalize to the same version. - * Tolerates prefix/format drift the GitHub feed routinely produces - * (e.g. release tag `v3.1.3` vs system-reported `3.1.3`, - * `release-1.2.0` vs `1.2.0`, `1.2.3+sha.abcd` vs `1.2.3`). - * - * Two empty/null/blank inputs are treated as equal — this is fine - * for the UI-text guards that use this (don't render a redundant - * "installed: …" subtext when there's nothing meaningful to show). - * Callers that need a stricter "both present and equal" check - * should compare [normalizeVersion] against `""` first. - */ fun isSameVersion(a: String?, b: String?): Boolean = compareVersions(a, b) == 0 - /** - * Strict literal equality after the conservative cleanup pass that - * [stripCommonPrefixes] (delegating to [stripFullPrefix]) applies: - * trim, strip `refs/tags/`, strip a single case-insensitive - * word-style prefix with separator (`version-`, `release/`, `app_`, - * `build-`, `ver.`), strip a leading `v` / `V`, trim again. - * - * Differs from [isSameVersion] in that it does NOT strip `+build` - * metadata, does NOT extract a dotted-digit core from arbitrarily - * prefixed tags, and is case-sensitive on the suffix. Use this in - * UI branches that gate "Open" vs "Install" CTAs — semver treats - * `1.0.0+build.1` and `1.0.0+build.2` as equivalent for ordering, - * but users (and maintainers who abuse build metadata to ship - * distinct artifacts under the same numeric core) consider them - * different versions. - * - * Two null/blank inputs return `false`. The check requires both - * sides to be present; otherwise the caller would gate UI on - * "two unknowns are the same", which is never the intent. - */ fun isExactSameVersion(a: String?, b: String?): Boolean { val cleanedA = stripCommonPrefixes(a) ?: return false val cleanedB = stripCommonPrefixes(b) ?: return false @@ -278,8 +124,7 @@ object VersionMath { if (parsedA != null && parsedB != null) { return compareSemver(parsedA, parsedB) } - // Neither is parseable as semver — last-resort lexicographic - // comparison. Callers should treat this as low-confidence. + return a.compareTo(b) } @@ -290,26 +135,15 @@ object VersionMath { val bi = b.numbers.getOrElse(i) { 0L } if (ai != bi) return ai.compareTo(bi) } - // Numeric parts equal — spec: stable > pre-release when - // pre-release only present on one side. + return when { a.preRelease == null && b.preRelease == null -> 0 - a.preRelease == null -> 1 // a has no pre, so a > b + a.preRelease == null -> 1 b.preRelease == null -> -1 else -> comparePreRelease(a.preRelease, b.preRelease) } } - /** - * Compare pre-release identifiers per semver spec: - * - Identifiers consisting of only digits are compared - * numerically. - * - Identifiers with letters are compared lexically. - * - Numeric identifiers always have lower precedence than - * alphanumeric. - * - A larger set of pre-release fields has higher precedence if - * all preceding are equal. - */ private fun comparePreRelease(a: String, b: String): Int { val aParts = a.split(".") val bParts = b.split(".") @@ -321,7 +155,7 @@ object VersionMath { val cmp = when { aNum != null && bNum != null -> aNum.compareTo(bNum) - aNum != null -> -1 // numeric < alphanumeric + aNum != null -> -1 bNum != null -> 1 else -> ap.compareTo(bp) } @@ -353,80 +187,26 @@ object VersionMath { private val DOTTED_DIGIT_PATTERN = Regex("""\d+(?:\.\d+)*(?:-[\w.]+)?""") - /** - * Word-style tag prefix that some maintainers use instead of the - * usual leading `v`. Recognises a single leading word followed by - * an optional separator and the version core. Case-insensitive so - * `Release_1.2.0`, `release/1.2.0`, `App-1.2.0` all collapse. - * - * The trailing class allows `-`, `_`, `/`, `.` or whitespace as - * separators; the regex explicitly does NOT match the bare prefix - * with no separator (`version1.2.3`) — that's an unusual format - * and safer left to the dotted-digit fallback. - */ private val VERSION_WORD_PREFIX = Regex( """^(version|release|app|build|ver)\s*[-_/.]\s*""", RegexOption.IGNORE_CASE, ) - /** - * Hyphenated calver: `2024-10-15`, `2024-3-1`, optionally followed - * by a trailing identifier (`2024-10-15-rc1`). Year is constrained - * to 1900–2199 to avoid swallowing semver pre-release identifiers - * that start with a small integer (`1.0-10-rc1` should NOT be - * treated as the year 10). - */ private val CALVER_HYPHEN_PATTERN = Regex("""^((?:19|20|21)\d{2})-(\d{1,2})-(\d{1,2})(?:[-.](.+))?$""") - /** - * Catches `1.2.0beta01` / `2.0RC1` / `0.9preview2` — a digit - * directly followed by a letter, no separator. Used to insert the - * missing hyphen ONLY when the tail starts with a known - * pre-release marker (architecture suffixes like `1.2.0arm64` are - * left intact). - */ private val ADJACENT_ALPHA_PATTERN = Regex("""\d[A-Za-z]""") - /** - * JetBrains-style milestone shorthand match used in - * [insertHyphenBeforeKnownMarker]. Matches `m1`, `m12`, `M5`, - * etc. at the start of a tail like `m5-arm64`. Kept separate - * from [KNOWN_PRE_RELEASE_PREFIXES] because that list is - * `startsWith`-friendly literal prefixes; this one needs a - * regex. - */ private val M_DIGIT_TAIL_PATTERN = Regex("""^m\d+""", RegexOption.IGNORE_CASE) - /** - * 8-digit date integer like `20260502`. Year constrained to - * 1900-2199 to keep this from swallowing arbitrary 8-digit - * integers that maintainers might use as monotonic build numbers - * unrelated to the calendar. - */ private val DATE_INTEGER_PATTERN = Regex("""(?:19|20|21)\d{2}\d{2}\d{2}""") - /** - * Dotted calver — `2024.10.15`, optionally with a trailing build - * identifier (`2024.10.15.4567`). Year guard same as [DATE_INTEGER_PATTERN]. - */ private val DOTTED_CALVER_PATTERN = Regex("""(?:19|20|21)\d{2}\.\d{1,2}\.\d{1,2}(?:\.\d+)?""") - /** - * Bare commit-hash style tag (7-40 lowercase hex chars). Some - * repositories use commit SHAs as release tags — these never - * compare meaningfully but should be classified as such so UIs - * can render them as "build" rather than as a version number. - */ private val COMMIT_HASH_PATTERN = Regex("""[0-9a-f]{7,40}""") - /** - * Marker prefixes recognised when separating an adjacent-letter - * pre-release. Mirrors [PRE_RELEASE_MARKER_PATTERN] but as plain - * strings for the `startsWith` check. - */ private val KNOWN_PRE_RELEASE_PREFIXES = listOf( "alpha", @@ -443,81 +223,13 @@ object VersionMath { "pre", ) - /** - * Heuristic: returns `true` when [tag] contains a well-known - * pre-release marker. - * - * Why this exists: the GitHub API exposes a `prerelease: bool` - * flag on every release, but **maintainers regularly forget to - * set it**. A release tagged `v2.0.0-rc.1` with `prerelease: - * false` is still semantically a pre-release, and surfacing it - * as a stable update to opted-out users is a silent foot-gun. - * `GithubRelease.isEffectivelyPreRelease()` combines the API flag - * with this tag heuristic so one is enough. - * - * Recognised markers (case-insensitive), preceded by `-`, `.`, - * or `_`, and followed by a separator, digit, or end-of-string: - * - `alpha`, `beta`, `rc` — classic semver pre-release labels - * - `preview`, `snapshot`, `canary`, `nightly` — CI / early builds - * - `milestone` / `m\d+` — JetBrains-style milestone builds - * - `ea` — early access (Oracle / JetBrains / vendor convention) - * - `dev` — dev build shorthand - * - `pre` — generic pre-release prefix when followed by digit or dot - * - * Intentionally **not** recognised (too ambiguous / too many - * false positives): - * - `test` (`-test-build` is often a real release artefact) - * - `a\d+` / `b\d+` alone (collides with `-arm64`, `-amd64`, etc.) - * - `stable` / `release` (explicit non-markers) - * - * Examples that match: - * `v1.2.3-beta`, `1.2.3-alpha.1`, `v2-rc.2`, `1.0.0-preview2`, - * `2025.04-nightly`, `v1.0.0-canary.3`, `1.0-m5`, - * `0.9.0-snapshot`, `7.0-ea` - * - * Examples that DO NOT match: - * `v1.2.3`, `1.2.3-stable`, `v1.2.3-android`, `release-1.2.3`, - * `v2.0-final`, `v1.0.0-test-3` - */ fun isPreReleaseTag(tag: String?): Boolean { if (tag.isNullOrBlank()) return false - // Pre-process so the regex's `\b` boundary catches markers that - // sit flush against the numeric core (e.g. `1.2.0beta01`) — the - // regex itself stays anchored on word boundaries to keep the - // false-positive rate low for embedded substrings. + val separated = insertHyphenBeforeKnownMarker(tag) return PRE_RELEASE_MARKER_PATTERN.containsMatchIn(separated) } - /** - * Returns the canonical label for the first pre-release marker - * found in [tag], or `null` if none. Intended for UI badges that - * want to show "Beta" / "Alpha" / "RC" instead of a generic - * "Pre-release" pill — a much better signal for users deciding - * whether to install. - * - * Labels are returned in title-case regardless of how they were - * spelled in the tag (so `V1.0-BETA` and `v1.0-beta` both - * resolve to `"Beta"`). - * - * Mapping rules: - * - `alpha` → `Alpha` - * - `beta` → `Beta` - * - `rc` / `rc\d+` → `RC` - * - `preview` → `Preview` - * - `prerelease` → `Pre-release` - * - `snapshot` → `Snapshot` - * - `canary` → `Canary` - * - `nightly` → `Nightly` - * - `milestone` / `m\d+` → `Milestone` - * - `ea` → `Early Access` - * - `dev` → `Dev` - * - `pre` → `Pre` - * - * Callers that also want to treat the API `prerelease` flag as - * authoritative should use this alongside - * [zed.rainxch.core.domain.model.isEffectivelyPreRelease]. - */ fun preReleaseMarkerLabel(tag: String?): String? { if (tag.isNullOrBlank()) return null val separated = insertHyphenBeforeKnownMarker(tag) @@ -541,50 +253,26 @@ object VersionMath { } private val PRE_RELEASE_MARKER_PATTERN = - // `\b` word boundaries cleanly separate markers from the - // surrounding tag (so `alpha` matches `v1.0-alpha` but not - // `alphabet`). The trailing `\d*` allows shorthand suffixes - // like `rc1`, `beta2`, `preview3` without requiring a - // separator between the word and the number. Longer - // alternatives (`prerelease`) come before shorter prefixes - // (`pre`) so the regex engine finds the longest match. + Regex( "\\b(alpha|beta|rc|preview|prerelease|snapshot|canary|nightly|milestone|ea|dev|pre|m\\d+)\\d*\\b", RegexOption.IGNORE_CASE, ) - /** - * Coarse classification of the versioning scheme a tag string - * appears to follow. Useful for UI surfaces that want to render - * a date-stamped release differently from a semver one ("Released - * 2024-10-15" vs "Version 1.2.3"), or warn when a maintainer - * appears to have switched schemes mid-history (which silently - * breaks ordering — `1.2.0` would always read as older than - * `20260502` under numeric semver compare even if it was tagged - * later). - * - * The classification is intentionally rough; the underlying - * comparator does NOT branch on it. Callers can combine - * [detectScheme] outputs from two tags to detect cross-scheme - * comparisons that warrant a UI hint. - */ fun detectScheme(version: String?): Scheme { if (version.isNullOrBlank()) return Scheme.Unknown val cleaned = stripFullPrefix(version).substringBefore('+') if (cleaned.isEmpty()) return Scheme.Unknown - // Hyphenated calver — yyyy-mm-dd, optionally with a trailing - // identifier we don't care about for the classification. + if (CALVER_HYPHEN_PATTERN.matchEntire(cleaned) != null) return Scheme.CalVer - // Single 8-digit run looks like yyyymmdd, e.g. `20260502`. + DATE_INTEGER_PATTERN.matchEntire(cleaned)?.let { return Scheme.CalVer } - // Dotted calver — yyyy.mm.dd inside a semver-shaped string. + DOTTED_CALVER_PATTERN.matchEntire(cleaned)?.let { return Scheme.CalVer } - // Anything that parses as semver after our normalisation pass - // is semver, including adjacent-letter pre-release variants. + val separated = insertHyphenBeforeKnownMarker(cleaned) if (parseSemanticVersion(separated) != null) return Scheme.SemVer - // Hex-ish commit pointers (`v1.2.0+abc1234` strips the build - // metadata, but a bare commit hash falls here). + if (COMMIT_HASH_PATTERN.matchEntire(cleaned) != null) return Scheme.CommitHash return Scheme.Unknown } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt index 9105f380c..b45acb46c 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/util/applyThemeAwareImages.kt @@ -7,18 +7,14 @@ fun applyThemeAwareImages( if (content.isEmpty()) return content var processed = content - // Strip markdown-image entries whose URL has `#gh-dark-mode-only` - // or `#gh-light-mode-only` and we're on the OTHER theme. GitHub's - // own renderer hides these on the mismatched theme; emulating that - // keeps READMEs from showing both light + dark variants stacked. processed = processed.replace( Regex("""!\[([^\]]*)\]\(([^)]*?)#gh-(dark|light)-mode-only([^)]*)\)"""), ) { match -> val mode = match.groupValues[3] if ((isDark && mode == "light") || (!isDark && mode == "dark")) { - "" // drop entirely; alt text would just be noise here + "" } else { - // Strip the fragment so the URL is clean for Coil's cache key. + val alt = match.groupValues[1] val urlBase = match.groupValues[2] val trailing = match.groupValues[4] @@ -26,7 +22,6 @@ fun applyThemeAwareImages( } } - // Same for raw HTML tags. processed = processed.replace( Regex( """]*?)src\s*=\s*(["'])([^"']*?)#gh-(dark|light)-mode-only([^"']*?)\2([^>]*?)>""", diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt index 6f365696c..9680379a0 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt @@ -22,19 +22,13 @@ fun ExpressiveCard( onLongClick: (() -> Unit)? = null, content: @Composable () -> Unit, ) { - // Long-press without tap leaves the gesture orphaned: the card looks - // tappable but only responds to a hold. Fail loud so the API contract - // is obvious at the call site. + check(onLongClick == null || onClick != null) { "ExpressiveCard: onLongClick requires onClick" } when { onClick != null && onLongClick != null -> { - // ElevatedCard's built-in `onClick` doesn't expose long-press; - // route both gestures through `combinedClickable`. Clip the - // modifier chain to the card shape FIRST so the ripple - // respects the 32.dp rounded corners — without the clip the - // ripple draws as a square overlapping the card edges. + ElevatedCard( modifier = modifier diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index ba599b5d1..0730d2676 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -414,8 +414,7 @@ private fun RepositoryActionsBottomSheet( containerColor = MaterialTheme.colorScheme.surfaceContainerLow, ) { Column(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp)) { - // Context header so the user can verify which repo they're - // acting on without the card behind the sheet. + Row( modifier = Modifier diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.kt index c3ccfaa59..a060cc128 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.kt @@ -5,11 +5,6 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -/** - * Wraps content with a platform-appropriate scrollbar. - * On Desktop (JVM), adds a VerticalScrollbar when [enabled] is true. - * On Android, renders only the [content] (no scrollbar). - */ @Composable expect fun ScrollbarContainer( listState: LazyListState, @@ -18,9 +13,6 @@ expect fun ScrollbarContainer( content: @Composable () -> Unit, ) -/** - * Overload for [LazyStaggeredGridState]. - */ @Composable expect fun ScrollbarContainer( gridState: LazyStaggeredGridState, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt index 8c9c80231..28345f346 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt @@ -37,7 +37,7 @@ fun CriticalAnnouncementModal( onOpenDetails: () -> Unit, ) { AlertDialog( - onDismissRequest = { /* non-dismissible */ }, + onDismissRequest = { }, icon = { Icon( imageVector = Icons.Filled.Security, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt index 601391786..97617edf7 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/IconButton.kt @@ -10,10 +10,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -/** - * 36×36 transparent icon button. Back / Share / Favorite / More (DESIGN.md §7.1). - * Min 48dp touch target preserved via padding inside the click area. - */ @Composable fun IconButton( onClick: () -> Unit, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt index 9cf00b7ad..70658f84c 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/OutlineButton.kt @@ -17,10 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -/** - * Outline tertiary action — transparent fill, 1dp outline ring, pill-shaped. - * Inspect / Refresh / Filter / Cancel (DESIGN.md §7.1). - */ @Composable fun OutlineButton( onClick: () -> Unit, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt index 413db5f2c..093319b8b 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/PrimaryButton.kt @@ -28,11 +28,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape -/** - * Primary CTA — wonky asymmetric squircle filled with `primary`. Reserved for the - * single most important action on a surface (DESIGN.md §7.1): Install / Update / - * Open / Get / Sign in. Spring-physics press feedback per D10. - */ @Composable fun PrimaryButton( onClick: () -> Unit, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt index ef0a64110..4c9d511a4 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/TintedButton.kt @@ -19,11 +19,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Tinted secondary CTA — `primaryContainer` background, `primary` text. Asymmetric - * (non-wonky) squircle. Used for Get / Read more / secondary install-panel actions - * (DESIGN.md §7.1). - */ @Composable fun TintedButton( onClick: () -> Unit, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt index db0a81140..30939df39 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/CompactCard.kt @@ -12,11 +12,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Compact card archetype — surface bg, dashed-border footer (composed by caller), - * card-sized asymmetric squircle (DESIGN.md §7.3). Used by Hot release cards, - * Trending cards, etc. Caller supplies internal layout via slot. - */ @Composable fun CompactCard( modifier: Modifier = Modifier, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt index c7dcb9982..677ec7302 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/LeadHeroCard.kt @@ -16,12 +16,6 @@ import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.vocabulary.AppAccent -/** - * Lead/hero card — full-width, accent-tinted bg, soft radial accent bloom, wonky - * squircle shape (DESIGN.md §7.3). Used for the top Hot release card on Home and - * featured items. Bloom uses [accent.c] at low alpha; DESIGN.md §2.5 explicitly - * permits this as editorial flair. - */ @Composable fun LeadHeroCard( accent: AppAccent, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt index d14425eb3..8acf9346f 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RowCard.kt @@ -14,11 +14,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Dense list row — surface bg + squircle radius. Used by Library, Most Popular, - * Search results (DESIGN.md §7.4). 10–12dp padding, 11dp gap. Caller supplies - * row content via slot. - */ @Composable fun RowCard( modifier: Modifier = Modifier, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt index 40a2316a3..4bb6c07fe 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt @@ -21,12 +21,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * 2×2 vital-signs grid on Detail pages (DESIGN.md §7.7). Fixed slots: - * Released · Maintained · Stars · Permissions. - * - * Each [VitalTile] takes glyph + value (signal-colored Fraunces italic 13) + label. - */ @Composable fun VitalSignsGrid( released: VitalTile, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt index ba5b22d26..cfd9f997e 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt @@ -21,12 +21,6 @@ import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.vocabulary.WaxSeal import zed.rainxch.core.presentation.vocabulary.WaxSealState -/** - * Trust card anchored to the install panel (DESIGN.md §7.8). Wax-seal glyph + - * Fraunces italic state label + JetBrains Mono fingerprint detail. Backgrounds - * follow the seal state — successT tint for intact, dangerT for cracked, surface - * for open. The cracked state is the ONLY place red is aggressive in the UI. - */ @Composable fun WaxSealTrustCard( state: WaxSealState, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt index 8edf221c5..35f4862d2 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/AddChip.kt @@ -21,10 +21,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Dashed "+ Add filter" affordance (DESIGN.md §7.2). Same dimensions as [FilterChip] - * but with a dashed outline instead of solid. - */ @Composable fun AddChip( label: String, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt index 4bff7fc0b..9cdca67d8 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/FilterChip.kt @@ -18,11 +18,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Active/inactive filter chip (DESIGN.md §7.2). Active = tintP bg + primary text + - * 1dp primary-tinted border. Inactive = transparent + outline border + ink text. - * Optional `×` chip — caller composes via dismiss arg. - */ @Composable fun FilterChip( label: String, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt index cbe8df036..bd16dddc5 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsBottomSheet.kt @@ -19,13 +19,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape -/** - * Wonky-top bottom sheet (DESIGN.md §16.1). Wraps Material 3's [ModalBottomSheet] - * with the project's asymmetric top corners + drag handle. - * - * Caller composes title + content + action row via slots. Use right-aligned primary - * (wonky) + outline Cancel pair in the action row. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun GhsBottomSheet( diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt index 3a8835098..3b1e51f7e 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt @@ -25,11 +25,6 @@ import zed.rainxch.core.presentation.components.buttons.PrimaryButton import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.vocabulary.Squiggle -/** - * Modal confirm dialog (DESIGN.md §16.2). Cancel-left, Confirm-right convention. - * Destructive confirms pass `destructive = true` to swap the primary fill to - * danger color. - */ @Composable fun GhsConfirmDialog( title: String, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt index 0ae1a89b2..adcf11d1f 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt @@ -16,10 +16,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii -/** - * Floating dropdown panel (DESIGN.md §16.7). Surface bg, wonky-soft squircle, 1dp - * outline, soft shadow. Items composed by caller using [GhsDropdownItem]. - */ @Composable fun GhsDropdownMenu( expanded: Boolean, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt index d9bf02b81..81c88ff1c 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsFullScreenSheet.kt @@ -17,11 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.components.buttons.IconButton -/** - * Full-screen sheet for multi-step flows (DESIGN.md §16.5) — OAuth device-flow, - * PAT entry, ExternalImport wizard. Back-arrow header, no title text (the big - * identity mark serves as the title). Caller composes body via slot. - */ @Composable fun GhsFullScreenSheet( onBack: () -> Unit, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt index 5e8fbc0cb..20f85a8c8 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsToast.kt @@ -15,14 +15,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape -/** Toast tint variants (DESIGN.md §16.3). */ enum class ToastTint { Default, Success, Error, Info } -/** - * Wonky-squircle toast (DESIGN.md §16.3). Pure visual layer — caller wires - * `SnackbarHost` / `SnackbarHostState` for lifecycle. Use [Default] for surface - * neutral, [Success] / [Error] for tinted, [Info] for primary tint. - */ @Composable fun GhsToast( modifier: Modifier = Modifier, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt index 52ce732e2..24289ede2 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/Banner.kt @@ -14,13 +14,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii -/** Banner tint variants per DESIGN.md §7.6. */ enum class BannerTint { Info, Success, Warning, Danger } -/** - * Inline banner for clipboard detection, update available, integrity warnings, etc. - * (DESIGN.md §7.6). Composes glyph + body + optional trailing action via slot APIs. - */ @Composable fun Banner( tint: BannerTint = BannerTint.Info, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalScrollbarEnabled.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalScrollbarEnabled.kt index 5453d907c..804bc7606 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalScrollbarEnabled.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalScrollbarEnabled.kt @@ -2,8 +2,4 @@ package zed.rainxch.core.presentation.locals import androidx.compose.runtime.compositionLocalOf -/** - * CompositionLocal providing whether the scrollbar should be shown. - * Defaults to false (no scrollbar). - */ val LocalScrollbarEnabled = compositionLocalOf { false } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt index a54873053..39ca35001 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/model/GithubRepoSummaryUi.kt @@ -21,8 +21,6 @@ data class GithubRepoSummaryUi( val isFork: Boolean = false, val availablePlatforms: ImmutableList = persistentListOf(), val downloadCount: Long = 0, - // Non-GitHub forge host (codeberg.org, gitea.com, etc.). null for - // canonical GitHub repos. Travels with the model so DetailsScreen - // can route through the right API. + val sourceHost: String? = null, ) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt index cdf58f196..6bf9d1849 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Locals.kt @@ -3,11 +3,6 @@ package zed.rainxch.core.presentation.theme import androidx.compose.runtime.staticCompositionLocalOf import zed.rainxch.core.presentation.theme.tokens.Tokens -/** - * Composition locals exposing the design tokens that Material 3 doesn't natively cover. - * Provided by `GithubStoreTheme` / `GhsTheme`. Reading any of these outside a theme - * scope falls back to a sensible default rather than throwing. - */ val LocalPalette = staticCompositionLocalOf { Tokens.Nord.light } val LocalStatusColors = staticCompositionLocalOf { defaultStatusColors } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index 1b7426b62..ca591c133 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -12,15 +12,6 @@ import zed.rainxch.core.presentation.theme.tokens.Tokens import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor import zed.rainxch.core.presentation.utils.toTokenPalette -/** - * App-wide theme entry point. Resolves the active [AppTheme] palette + light/dark/amoled - * mode to a Material 3 [ColorScheme] backed by the design tokens in - * [zed.rainxch.core.presentation.theme.tokens.Tokens], plus provides composition locals - * that expose richer surfaces (status colors, thresholds, motion, spacing). - * - * `Main.kt` is the only call site — kept under the legacy `GithubStoreTheme` name until - * P6 chrome polish swaps in the user-facing rename to `GhsTheme`. - */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun GithubStoreTheme( diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt index 24a9edd17..2e0590972 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt @@ -17,15 +17,6 @@ import zed.rainxch.githubstore.core.presentation.res.fraunces_italic import zed.rainxch.githubstore.core.presentation.res.inter_tight import zed.rainxch.githubstore.core.presentation.res.jetbrains_mono -/** - * Three font families per DESIGN.md §3.1: - * - **Fraunces** (italic, weight 600): repo names, section titles, lead numbers. - * - **Inter Tight** (400–700): body text, labels, descriptions, button labels. - * - **JetBrains Mono** (500–700): versions, hashes, package names — technical artifacts. - * - * Variable .ttf files cover all weights at runtime via Compose's - * `Font(resource, weight)` variation resolution. - */ val fraunces @Composable get() = FontFamily( Font(Res.font.fraunces, FontWeight.Medium, FontStyle.Normal), @@ -53,15 +44,6 @@ val jetbrainsMono private val baseline = Typography() -/** - * Material 3 [Typography] mapped to GHS voices: - * - **display / headline / title** → Fraunces italic (the warm editorial voice). - * - **body / label** → Inter Tight (crisp, dense). - * - * `JetBrains Mono` is reserved for inline technical text (version tags, hashes) and - * is not part of the global Typography — components opt-in via the `jetbrainsMono` - * family directly. - */ @Composable fun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography { if (fontTheme == FontTheme.SYSTEM) return baseline @@ -83,7 +65,7 @@ fun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography { ) return Typography( - // Editorial voice — Fraunces italic + displayLarge = baseline.displayLarge.fraunces(FontWeight.SemiBold).copy( letterSpacing = (-0.025).em, fontSize = 36.sp, @@ -96,7 +78,7 @@ fun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography { titleLarge = baseline.titleLarge.fraunces(FontWeight.SemiBold).copy(fontSize = 18.sp), titleMedium = baseline.titleMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 16.sp), titleSmall = baseline.titleSmall.fraunces(FontWeight.SemiBold).copy(fontSize = 14.sp), - // Body voice — Inter Tight + bodyLarge = baseline.bodyLarge.sans(FontWeight.Normal).copy(fontSize = 14.sp), bodyMedium = baseline.bodyMedium.sans(FontWeight.Normal).copy(fontSize = 13.sp), bodySmall = baseline.bodySmall.sans(FontWeight.Medium).copy(fontSize = 12.sp), diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt index 650f989f0..10fffe142 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/shapes/WonkySquircleShape.kt @@ -11,23 +11,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -/** - * Hand-shaped asymmetric corner with elliptical (x ≠ y) radii — the "wonky squircle" - * from DESIGN.md §5.2 / §7.7 / §16.x. Compose's `RoundedCornerShape` cannot express - * elliptical corners (it's circular only), so this `Shape` builds a [Path] manually - * using `arcTo` with rectangular bounds whose width and height are independent. - * - * Each corner takes `(rxDp, ryDp)` — the horizontal and vertical sweep of that corner's - * ellipse arc. The CSS notation `border-radius: 20 14 22 16 / 16 22 14 20` translates to: - * topStart = (20, 16) - * topEnd = (14, 22) - * bottomEnd = (22, 14) - * bottomStart = (16, 20) - * - * Three preset constants ([CtaPrimary], [CtaAlt], [Search]) match the tokens.json - * `shape.wonkySquircle` block. For ad-hoc corners (sheets / dialogs / toasts), use - * the [WonkySquircleShape] constructor directly. - */ class WonkySquircleShape( private val topStart: CornerRadii, private val topEnd: CornerRadii, @@ -51,9 +34,9 @@ class WonkySquircleShape( val bsY = bottomStart.ry.toPx().coerceAtMost(size.height / 2f) val path = Path().apply { - // Start at top-start corner end-point (after sweeping in) + moveTo(tsX, 0f) - // Top edge → top-end corner + lineTo(size.width - teX, 0f) arcToCorner( cornerCenter = Offset(size.width - teX, teY), @@ -62,7 +45,7 @@ class WonkySquircleShape( startAngle = 270f, sweep = 90f, ) - // Right edge → bottom-end corner + lineTo(size.width, size.height - beY) arcToCorner( cornerCenter = Offset(size.width - beX, size.height - beY), @@ -71,7 +54,7 @@ class WonkySquircleShape( startAngle = 0f, sweep = 90f, ) - // Bottom edge → bottom-start corner + lineTo(bsX, size.height) arcToCorner( cornerCenter = Offset(bsX, size.height - bsY), @@ -80,7 +63,7 @@ class WonkySquircleShape( startAngle = 90f, sweep = 90f, ) - // Left edge → top-start corner + lineTo(0f, tsY) arcToCorner( cornerCenter = Offset(tsX, tsY), @@ -96,7 +79,7 @@ class WonkySquircleShape( } companion object { - /** `tokens.json.shape.wonkySquircle.css`: 20 14 22 16 / 16 22 14 20 */ + val CtaPrimary = WonkySquircleShape( topStart = CornerRadii(20.dp, 16.dp), topEnd = CornerRadii(14.dp, 22.dp), @@ -104,7 +87,6 @@ class WonkySquircleShape( bottomStart = CornerRadii(16.dp, 20.dp), ) - /** `tokens.json.shape.wonkySquircle.alt`: 22 16 24 18 / 18 24 16 22 */ val CtaAlt = WonkySquircleShape( topStart = CornerRadii(22.dp, 18.dp), topEnd = CornerRadii(16.dp, 24.dp), @@ -112,7 +94,6 @@ class WonkySquircleShape( bottomStart = CornerRadii(18.dp, 22.dp), ) - /** `tokens.json.shape.wonkySquircle.search`: 24 18 26 20 / 18 24 20 26 */ val Search = WonkySquircleShape( topStart = CornerRadii(24.dp, 18.dp), topEnd = CornerRadii(18.dp, 24.dp), @@ -120,7 +101,6 @@ class WonkySquircleShape( bottomStart = CornerRadii(20.dp, 26.dp), ) - /** Sheet — square bottom corners (flush to screen edge). */ val Sheet = WonkySquircleShape( topStart = CornerRadii(24.dp, 18.dp), topEnd = CornerRadii(18.dp, 24.dp), @@ -128,7 +108,6 @@ class WonkySquircleShape( bottomStart = CornerRadii(0.dp, 0.dp), ) - /** Dialog — symmetric-ish wonky. */ val Dialog = WonkySquircleShape( topStart = CornerRadii(28.dp, 22.dp), topEnd = CornerRadii(22.dp, 28.dp), @@ -136,7 +115,6 @@ class WonkySquircleShape( bottomStart = CornerRadii(24.dp, 26.dp), ) - /** Toast — compact wonky. */ val Toast = WonkySquircleShape( topStart = CornerRadii(18.dp, 14.dp), topEnd = CornerRadii(14.dp, 22.dp), diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt index 7476a2199..cfea4433c 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Radii.kt @@ -3,17 +3,6 @@ package zed.rainxch.core.presentation.theme.tokens import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.unit.dp -/** - * Asymmetric corner-radius scale from tokens.json. Pairs are (primary, secondary) - * applied diagonally — Compose uses `RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)`. - * The handoff's `radD(primary, secondary)` translates to topStart=primary, topEnd=secondary, - * bottomEnd=primary, bottomStart=secondary. - * - * For "wonky" CTAs / lead cards / search input / sheets / dialogs / toasts, use - * [zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape] in core/components. - * Compose's `RoundedCornerShape` cannot express the elliptical (x/y differ) corners - * required for wonkiness — that lands in P5. - */ object Radii { val chip = shape(11, 8) val row = shape(13, 10) @@ -23,7 +12,6 @@ object Radii { val hero = shape(24, 18) val heroLg = shape(28, 22) - /** Build an asymmetric squircle with diagonally-paired (primary, secondary) radii. */ fun shape(primary: Int, secondary: Int) = RoundedCornerShape( topStart = primary.dp, topEnd = secondary.dp, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt index 91426f6b2..c4bd7a4b6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Schemes.kt @@ -5,25 +5,6 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color -/** - * Maps a [Tokens.PaletteColors] to a Material 3 [ColorScheme] so existing M3 components - * (Button, Card, TextField, ...) keep working unchanged. Tokens not natively expressed - * in M3 (status colors, custom motion, shape radii) are exposed via the composition - * locals provided by GhsTheme — this mapper only covers M3 slots. - * - * Mapping rules: - * bg → background - * surface → surface / surfaceContainerLow / surfaceBright - * surface2 → surfaceVariant / surfaceContainer / surfaceContainerHigh - * ink → onBackground / onSurface - * ink2 → onSurfaceVariant - * outline → outline / outlineVariant - * primary → primary (foreground = bg in light, ink in dark for contrast) - * tintP → primaryContainer - * danger → error - * dangerT → errorContainer - * success* → exposed via LocalStatusColors only (M3 has no success slot) - */ fun toLightColorScheme(p: Tokens.PaletteColors): ColorScheme = lightColorScheme( primary = p.primary, onPrimary = Color.White, @@ -102,7 +83,6 @@ fun toDarkColorScheme(p: Tokens.PaletteColors): ColorScheme = darkColorScheme( surfaceContainerHighest = p.surface2, ) -/** Resolves a [Tokens.Palette] + [Tokens.Mode] to its M3 [ColorScheme]. */ fun colorSchemeFor(palette: Tokens.Palette, mode: Tokens.Mode): ColorScheme { val tokens = Tokens.palette(palette, mode) return if (mode == Tokens.Mode.LIGHT) toLightColorScheme(tokens) else toDarkColorScheme(tokens) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt index 480ae0f43..7f837d26c 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt @@ -2,14 +2,6 @@ package zed.rainxch.core.presentation.theme.tokens import androidx.compose.ui.graphics.Color -/** - * Hand-translated from `~/Downloads/handoff 4/tokens.json` (locked at DESIGN.md v1.0). - * Source of truth for the design system; do not introduce ad-hoc hex values elsewhere. - * - * Layout: palette tokens (Nord / Cream / Forest / Plum) × mode (Light / Dark / Amoled), - * status colors (palette-independent), thresholds (freshness / maintenance / stars), - * motion durations, spacing scale. - */ object Tokens { enum class Palette { NORD, CREAM, FOREST, PLUM } @@ -197,7 +189,6 @@ object Tokens { } } - /** Palette-independent status colors. Same hex regardless of theme. */ object Status { object Freshness { val hot = Color(0xFFE07856) @@ -223,7 +214,6 @@ object Tokens { } } - /** Bucket thresholds and ring-fraction targets from tokens.json. */ object Thresholds { data class FreshnessBucket( val maxDaysInclusive: Int?, @@ -262,7 +252,6 @@ object Tokens { ) } - /** Motion (durations in ms; pair with [Motion.heartbeatScaleFrom/To] for the breathing dot). */ object Motion { const val tapHighlightMs = 120 const val paletteCrossfadeMs = 250 @@ -278,7 +267,6 @@ object Tokens { const val heartbeatHaloToAlpha = 0.0f } - /** Spacing scale (Material 3 aligned defaults; expand only if a screen needs it). */ object Spacing { val xs = 4 val sm = 8 @@ -288,14 +276,6 @@ object Tokens { val xxl = 32 } - /** - * Canonical topic codes (14) emitted by the backend topic mapper. Frontend - * draws one glyph per code. Backend normalizes raw GitHub topics into this - * set — frontend does NOT keep an alias table anymore. - * - * If backend ships a code we don't have a glyph for yet, [TopicGlyph] silently - * renders nothing (graceful degrade until next frontend release). - */ object Topics { val supported = setOf( "security", "privacy", "networking", "ai", "notes", @@ -305,7 +285,6 @@ object Tokens { ) } - /** SPDX → posture map for [LicensePosture]. */ object Licenses { val copyleft = setOf( "AGPL-3.0", "GPL-3.0", "GPL-2.0", "LGPL-3.0", "LGPL-2.1", "MPL-2.0", diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ArrowKeyScroll.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ArrowKeyScroll.kt index fe490c935..f513fb335 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ArrowKeyScroll.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ArrowKeyScroll.kt @@ -22,9 +22,6 @@ import kotlinx.coroutines.launch private const val ARROW_STEP_PX = 120f private const val PAGE_STEP_FRACTION = 0.9f -// When `autoFocus = true`, the modifier requests keyboard focus on first -// composition so arrow keys work without a prior click/tab. Pass `false` on -// screens where a TextField should keep focus (e.g. search inputs). @Composable fun Modifier.arrowKeyScroll( listState: LazyListState, @@ -38,9 +35,7 @@ fun Modifier.arrowKeyScroll( scrollToBottom = { val last = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0) listState.animateScrollToItem(last) - // `animateScrollToItem` aligns the target to the viewport start, - // which can leave empty space after the last item. A follow-up - // large-delta scroll is clamped to the real end. + listState.animateScrollBy(Float.MAX_VALUE) }, ) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt index 0f4241ce5..6f39b5d90 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/TimeFormatters.kt @@ -14,15 +14,11 @@ import kotlin.time.Instant @OptIn(ExperimentalTime::class) private fun parseIsoInstantLenient(isoInstant: String): Instant? { - // Trim up front so `Instant.parse` doesn't choke on surrounding - // whitespace (which it treats as invalid) while `isBlank()` was - // already masking the empty-after-trim case. + val trimmed = isoInstant.trim() if (trimmed.isEmpty()) return null runCatching { return Instant.parse(trimmed) } - // Backend occasionally returns timestamps without seconds (e.g. "2024-10-16T17:00Z"). - // Retry after inserting ":00" before the timezone designator. val normalized = runCatching { val tzStart = trimmed.indexOfAny(charArrayOf('Z', '+', '-'), startIndex = 11) if (tzStart < 0) return@runCatching null diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt index a74b0aad0..f5799aeaa 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/AppAccent.kt @@ -2,33 +2,15 @@ package zed.rainxch.core.presentation.vocabulary import androidx.compose.ui.graphics.Color -/** - * Per-app accent triple (DESIGN.md §2.4). The accent travels with the repo across - * surfaces (lead card bg tint, freshness ring outer, install panel bloom). - * - * - `c` — saturated accent (the brand color) - * - `lt` — light tint (used in all light palettes as a 18–22% bg) - * - `dtAlpha` — alpha applied to `c` over the dark surface (22% per themes.md) - */ data class AppAccent(val c: Color, val lt: Color, val dtAlpha: Float = 0.22f) { fun tintFor(isDark: Boolean): Color = if (isDark) c.copy(alpha = dtAlpha) else lt } -/** - * Resolves a per-app accent from (in order): - * 1. Backend-supplied hex (when [backendHex] is non-null and parseable) - * 2. Topic-derived (first match in [TOPIC_ACCENTS]) - * 3. Language-derived ([LANGUAGE_ACCENTS]) - * 4. Blue fallback (Nord primary) - * - * Source-of-truth tables in UI-SPEC.md §6.1 / §6.2. Deterministic — same repo - * resolves to the same accent across launches and devices. - */ object AppAccentResolver { private val FALLBACK = AppAccent(c = Color(0xFF5E81AC), lt = Color(0xFFD8E1EC)) private val TOPIC_ACCENTS: Map = buildMap { - // Each pair is (canonical topic name → accent). Aliases listed inline. + val photo = AppAccent(Color(0xFF5E81AC), Color(0xFFD8E1EC)) listOf("photo", "photos", "gallery").forEach { put(it, photo) } @@ -85,15 +67,15 @@ object AppAccentResolver { topics: List = emptyList(), primaryLanguage: String? = null, ): AppAccent { - // 1. Backend + backendHex?.let { parseHexAccent(it) }?.let { return it } - // 2. Topic + topics.forEach { t -> TOPIC_ACCENTS[t.lowercase()]?.let { return it } } - // 3. Language + primaryLanguage?.let { LANGUAGE_ACCENTS[it] }?.let { return it } - // 4. Fallback + return FALLBACK } @@ -105,7 +87,7 @@ object AppAccentResolver { if (cleaned.length == 6) (0xFF000000 or intVal).toULong() else intVal.toULong() }.getOrNull() ?: return null val c = Color(rgb.toLong()) - // Synthetic light tint = c blended toward white at 78% + val lt = blendTowardWhite(c, 0.78f) return AppAccent(c, lt) } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt index 4d9142546..7842826de 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/CookieShape.kt @@ -7,14 +7,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection -/** - * 9-petal Material 3 Expressive flower silhouette. Used at exactly 3 touchpoints - * (DESIGN.md §4.3 + §5.3): brand "G" mark, user avatar tile, active bottom-nav tab. - * - * Path translated from `tokens.json.shape.cookie.path` (viewBox 100×100), rescaled - * to fill the [Size] passed by Compose. Used via `Modifier.clip(CookieShape)` or as - * a `Shape` parameter on `Surface`/`Box`. - */ object CookieShape : Shape { override fun createOutline( size: Size, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt index 5809593f0..9bcd8a2c0 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/DownloadWeight.kt @@ -10,10 +10,6 @@ import androidx.compose.ui.unit.dp import kotlin.math.log10 import kotlin.math.min -/** - * Log-scale dot inside a fixed-size ring. Radius = `log10(downloads)`. Replaces - * "62.8k downloads" prose with adoption magnitude (DESIGN.md §4.1). - */ @Composable fun DownloadWeight( downloads: Long, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt index 8caa6102e..2538ad404 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Freshness.kt @@ -3,7 +3,6 @@ package zed.rainxch.core.presentation.vocabulary import androidx.compose.ui.graphics.Color import zed.rainxch.core.presentation.theme.tokens.Tokens -/** Maintenance / freshness buckets (DESIGN.md §2.3, thresholds in Tokens). */ enum class FreshnessState { HOT, FRESH, WARM, COOL, DORMANT } data class Freshness(val state: FreshnessState, val color: Color, val ringFraction: Float) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt index 7c4234ab2..31a4576af 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/FreshnessRing.kt @@ -13,12 +13,6 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp -/** - * Squircle icon tile wrapped in a freshness ring that drains over time. Replaces - * "Released N days ago" prose (DESIGN.md §4.1). Ring fraction + color encoded by - * the bucket from [freshnessOf]. Drop-in for app avatars on Home, Library, and - * Detail hero. - */ @Composable fun FreshnessRing( daysSinceRelease: Int, @@ -65,7 +59,7 @@ private fun FreshnessArc( (size.height - radius * 2) / 2f, ) val arcSize = Size(radius * 2, radius * 2) - // Background track, low opacity + drawArc( color = color.copy(alpha = 0.14f), startAngle = 0f, @@ -75,7 +69,7 @@ private fun FreshnessArc( size = arcSize, style = Stroke(width = sw, cap = StrokeCap.Round), ) - // Foreground drain + drawArc( color = color, startAngle = -90f, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt index fa2ae0ed6..13c4124a6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Heartbeat.kt @@ -22,15 +22,6 @@ import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalMotion import zed.rainxch.core.presentation.theme.LocalStatusColors -/** - * Pulsing dot that animates per maintenance state. Periods (DESIGN.md §6.1): - * active 1.4s / recent 2.4s / quiet 4.2s / dormant = static grey (no animation). - * - * Per the android-compose-ui skill, animation drives a `graphicsLayer` to avoid - * recomposition; the surrounding [Box] hosts the layout. Don't pair with - * [FreshnessRing] in dense list rows — they compete for the same "is this alive" - * attention (DESIGN.md §6.1 closing rule). - */ @Composable fun Heartbeat( daysSinceCommit: Int, @@ -43,7 +34,7 @@ fun Heartbeat( val color = heartbeatColor(daysSinceCommit, status) if (periodMs == null) { - // Dormant — static muted dot + Box( modifier = modifier .size(sizeDp.dp) @@ -85,7 +76,7 @@ fun Heartbeat( modifier = modifier.size(sizeDp.dp), contentAlignment = Alignment.Center, ) { - // Halo via drawBehind to avoid recomposition + Box( modifier = Modifier .size(sizeDp.dp) @@ -98,7 +89,7 @@ fun Heartbeat( ) }, ) - // Dot scales via graphicsLayer (no recomposition) + Box( modifier = Modifier .size(sizeDp.dp) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt index e65221179..5a0f3e4d6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt @@ -17,10 +17,6 @@ import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.theme.jetbrainsMono import zed.rainxch.core.presentation.theme.tokens.Tokens -/** - * Filled © tile (copyleft) or dashed · tile (permissive). Replaces SPDX text label - * (DESIGN.md §4.1). Uses [Tokens.Licenses] SPDX → posture map. - */ @Composable fun LicensePosture( spdx: String?, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt index 4dfb2c88e..4223c4f99 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PermDot.kt @@ -9,13 +9,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalStatusColors -/** Permission risk classification mapped to a single colored dot. */ enum class PermLevel { LOW, MODERATE, HIGH } -/** - * Single-dot heat indicator for permission risk. Replaces "App permissions" wall-of-text - * (DESIGN.md §4.1). Optional 3px halo ring for emphasis on hero surfaces. - */ @Composable fun PermDot( level: PermLevel, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt index 7808e6be0..d1569218f 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt @@ -16,14 +16,8 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp -/** Supported platforms in the silent vocabulary. */ enum class PlatformKind { ANDROID, WINDOWS, MACOS, LINUX } -/** - * Filled silhouette when supported, dashed outline at 32% alpha when not. Replaces - * "Android · Windows · Linux" prose (DESIGN.md §4.1). Always monochrome — never - * carries the per-app accent. - */ @Composable fun PlatformGlyph( kind: PlatformKind, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt index 1a589d8e3..8f6eb043d 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/SignalBars.kt @@ -11,10 +11,6 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalStatusColors -/** - * 4 ascending bars — WiFi-style mirror / connection strength. Replaces "62 ms latency" - * prose (DESIGN.md §4.1). `level` in 0..4; bars above `level` use outline color. - */ @Composable fun SignalBars( level: Int, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt index 2078c36bc..5024568a5 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/Squiggle.kt @@ -12,11 +12,6 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp -/** - * Hand-drawn wavy underline. Default ~40×5 dp, 1.6dp stroke, primary color at 60%. - * One per section heading (DESIGN.md §4.3). Path translated from - * `tokens.json.shape.squiggle.path` (viewBox 40×5). - */ @Composable fun Squiggle( modifier: Modifier = Modifier, @@ -27,8 +22,7 @@ fun Squiggle( val sy = size.height / 5f val path = Path().apply { moveTo(1f * sx, 3f * sy) - // Initial quadratic + four smooth quadratics with reflected control points - // (T command in SVG): C1=(5,0.5), then reflect through each end-point. + quadraticTo(5f * sx, 0.5f * sy, 9f * sx, 3f * sy) smoothQuadTo(17f * sx, 3f * sy, prevControl = Offset(5f * sx, 0.5f * sy), prevEnd = Offset(9f * sx, 3f * sy)) smoothQuadTo(25f * sx, 3f * sy, prevControl = Offset(13f * sx, 5.5f * sy), prevEnd = Offset(17f * sx, 3f * sy)) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt index dfacc6220..bf46f6285 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/StarTier.kt @@ -13,11 +13,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * Michelin-style 1-5 star tier from `stargazersCount`. Log-scale buckets - * (DESIGN.md §4.1, thresholds in `Tokens.Thresholds.stars`): - * 1 ★ < 1k, 2 ★ ≥1k, 3 ★ ≥10k, 4 ★ ≥50k, 5 ★ ≥100k. Replaces "62.8k stars". - */ @Composable fun StarTier( stars: Int, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt index e6b132487..794cb4681 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/TopicGlyph.kt @@ -17,12 +17,6 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Tokens -/** - * Micro-pictogram per canonical topic code (DESIGN.md §4.2). Backend now emits a - * normalized topic-code set per repo (R12-v2); aliases are no longer resolved - * here. Monochrome — never carries the per-app accent. Renders nothing when - * [topic] isn't in the supported set ([Tokens.Topics.supported]). - */ @Composable fun TopicGlyph( topic: String, @@ -57,12 +51,8 @@ fun TopicGlyph( private fun DrawScope.scaled(viewBoxValue: Float) = viewBoxValue / 24f * size.minDimension -// ───────────────────────────────────────────────────────────────────────────── -// New glyphs (7) — canonical R12-v2 topic codes -// ───────────────────────────────────────────────────────────────────────────── - private fun DrawScope.drawSecurity(c: Color, s: Stroke) { - // Padlock — shackle arc on top, body below. + val shackle = Path().apply { moveTo(scaled(8f), scaled(11f)) lineTo(scaled(8f), scaled(8f)) @@ -77,12 +67,12 @@ private fun DrawScope.drawSecurity(c: Color, s: Stroke) { cornerRadius = CornerRadius(scaled(1.5f), scaled(1.5f)), style = s, ) - // Keyhole dot + drawCircle(color = c, radius = scaled(1.3f), center = Offset(scaled(12f), scaled(15f))) } private fun DrawScope.drawPrivacy(c: Color, s: Stroke) { - // Eye outline (almond) + pupil + diagonal strikethrough. + val eye = Path().apply { moveTo(scaled(3f), scaled(12f)) quadraticTo(scaled(12f), scaled(4.5f), scaled(21f), scaled(12f)) @@ -101,7 +91,7 @@ private fun DrawScope.drawPrivacy(c: Color, s: Stroke) { } private fun DrawScope.drawNetworking(c: Color, s: Stroke) { - // Three ascending WiFi-style arcs + dot + drawArc( color = c, startAngle = 215f, @@ -133,7 +123,7 @@ private fun DrawScope.drawNetworking(c: Color, s: Stroke) { } private fun DrawScope.drawAi(c: Color, s: Stroke) { - // 4-point spark (sparkle) + small companion dot + val spark = Path().apply { moveTo(scaled(12f), scaled(3f)) lineTo(scaled(14f), scaled(10f)) @@ -149,7 +139,7 @@ private fun DrawScope.drawAi(c: Color, s: Stroke) { } private fun DrawScope.drawNotes(c: Color, s: Stroke) { - // Pencil over paper — paper rect + diagonal pencil + drawRoundRect( color = c, topLeft = Offset(scaled(4f), scaled(5f)), @@ -182,7 +172,7 @@ private fun DrawScope.drawNotes(c: Color, s: Stroke) { } private fun DrawScope.drawMessaging(c: Color, s: Stroke) { - // Speech bubble — rounded rect + tail + drawRoundRect( color = c, topLeft = Offset(scaled(3f), scaled(4f)), @@ -200,7 +190,7 @@ private fun DrawScope.drawMessaging(c: Color, s: Stroke) { } private fun DrawScope.drawBrowser(c: Color, s: Stroke) { - // Compass — circle + needle (pointer) + drawCircle(color = c, radius = scaled(8f), center = Offset(scaled(12f), scaled(12f)), style = s) val needle = Path().apply { moveTo(scaled(12f), scaled(6f)) @@ -213,7 +203,7 @@ private fun DrawScope.drawBrowser(c: Color, s: Stroke) { } private fun DrawScope.drawSocial(c: Color, s: Stroke) { - // Two people — circles + shoulder arcs + drawCircle(color = c, radius = scaled(2.4f), center = Offset(scaled(9f), scaled(8f)), style = s) drawCircle(color = c, radius = scaled(2.4f), center = Offset(scaled(16f), scaled(9f)), style = s) val shoulderA = Path().apply { @@ -229,7 +219,7 @@ private fun DrawScope.drawSocial(c: Color, s: Stroke) { } private fun DrawScope.drawLauncher(c: Color) { - // 3×3 grid dots + val r = scaled(1.5f) listOf(7f, 12f, 17f).forEach { cx -> listOf(7f, 12f, 17f).forEach { cy -> @@ -238,10 +228,6 @@ private fun DrawScope.drawLauncher(c: Color) { } } -// ───────────────────────────────────────────────────────────────────────────── -// Existing glyphs (6) — kept verbatim -// ───────────────────────────────────────────────────────────────────────────── - private fun DrawScope.drawSelfHosted(c: Color, s: Stroke) { val p = Path().apply { moveTo(scaled(4f), scaled(12f)) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt index 4b304dcfd..d5ec6000f 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionDelta.kt @@ -13,14 +13,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalStatusColors -/** Patch (single dot) / Minor (two dots) / Major (bar + slash). */ enum class VersionDeltaKind { PATCH, MINOR, MAJOR } -/** - * Visual update-risk indicator. Replaces "v2.7.0 → v2.7.5" prose with a primitive - * (DESIGN.md §4.1). PATCH = green dot, MINOR = two amber dots, MAJOR = filled bar + - * slash. Pairs with [VersionStack] for "how far behind" magnitude. - */ @Composable fun VersionDelta( delta: VersionDeltaKind, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt index 71854f5bb..a5b8aa25d 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/VersionStack.kt @@ -9,10 +9,6 @@ import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalStatusColors import kotlin.math.min -/** - * Stack of bars — one per skipped release. Grows tall with distance from current, - * capped at 7. Replaces "5 versions behind" prose (DESIGN.md §4.1). - */ @Composable fun VersionStack( count: Int, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt index db5f49295..6c344297d 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/WaxSeal.kt @@ -14,15 +14,8 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.LocalStatusColors -/** Signing-fingerprint trust state. */ enum class WaxSealState { INTACT, CRACKED, OPEN } -/** - * Wax-stamp glyph for binary trust (DESIGN.md §7.8). INTACT = solid brown circle + - * inner check; CRACKED = red circle + lightning split (the ONLY aggressive red); - * OPEN = dashed grey ring (signing-fingerprint unknown). 22dp default; 36-44dp on - * Detail trust card. - */ @Composable fun WaxSeal( state: WaxSealState, @@ -49,7 +42,7 @@ fun WaxSeal( center = center, style = Stroke(width = 0.6f.dp.toPx()), ) - // Inner check mark + val path = Path().apply { moveTo(size.width * (8.5f / 24f), size.height * (12f / 24f)) lineTo(size.width * (11f / 24f), size.height * (14.3f / 24f)) diff --git a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.jvm.kt b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.jvm.kt index 67e24696e..69cb19699 100644 --- a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.jvm.kt +++ b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/components/ScrollbarContainer.jvm.kt @@ -86,10 +86,6 @@ actual fun ScrollbarContainer( } } -/** - * Custom [ScrollbarAdapter] for [LazyStaggeredGridState] since Compose Desktop - * does not provide a built-in [rememberScrollbarAdapter] overload for staggered grids. - */ private class StaggeredGridScrollbarAdapter( private val gridState: LazyStaggeredGridState, ) : ScrollbarAdapter { diff --git a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.jvm.kt b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.jvm.kt index cc838031f..eb55194b4 100644 --- a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.jvm.kt +++ b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/ApplyAndroidSystemBars.jvm.kt @@ -4,5 +4,5 @@ import androidx.compose.runtime.Composable @Composable actual fun ApplyAndroidSystemBars(isDarkTheme: Boolean?) { - // No-op + } diff --git a/feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt b/feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt index e5ca30f9e..635780ff8 100644 --- a/feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt +++ b/feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt @@ -255,20 +255,13 @@ class AppsRepositoryImpl( val now = Clock.System.now().toEpochMilliseconds() val globalPreRelease = tweaksRepository.getIncludePreReleases().first() val normalizedFilter = assetFilterRegex?.trim()?.takeIf { it.isNotEmpty() } - // Pre-derived fingerprint (from import) wins over re-deriving - // from the picked filename. Falls through to deriving fresh - // from the picked asset when there's nothing pre-computed. + val derivedVariant = preferredAssetVariant?.trim()?.takeIf { it.isNotEmpty() } ?: pickedAssetName?.let { AssetVariant.deriveFromPickedAsset(it, pickedAssetSiblingCount) } - // Multi-layer fingerprint: when coming from import, we already - // have these stored from the prior install. When coming from - // the link sheet picker, derive them fresh from the picked - // asset's filename so all three identity layers are populated - // atomically with the rest of the row. val freshFingerprint = if (preferredAssetTokens == null && assetGlobPattern == null && pickedAssetName != null) { AssetVariant.fingerprintFromPickedAsset(pickedAssetName, pickedAssetSiblingCount) @@ -573,10 +566,7 @@ class AppsRepositoryImpl( .filter { it.draft != true } .map { it.toDomain() } .filter { - // Use the same tag-aware pre-release filter as the - // GitHub path (`InstalledAppsRepositoryImpl`) so a - // `v2.0.0-rc1` release with a `prerelease: false` - // flag is still correctly excluded. + includePreReleases || !it.isEffectivelyPreRelease() } .maxByOrNull { it.publishedAt } diff --git a/feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt b/feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt index 8246c7048..f061a49d5 100644 --- a/feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt +++ b/feature/apps/domain/src/commonMain/kotlin/zed/rainxch/apps/domain/repository/AppsRepository.kt @@ -33,44 +33,16 @@ interface AppsRepository { repoInfo: GithubRepoInfo, assetFilterRegex: String? = null, fallbackToOlderReleases: Boolean = false, - /** - * Filename of the asset the user picked in the link sheet (or null - * if no picker was shown — e.g. the repo had no installable assets - * and the link is purely for tracking). When set, the implementation - * derives a stable variant tag from it via `AssetVariant` and - * persists it as `preferredAssetVariant`, so subsequent updates - * stay on the same variant. - */ + pickedAssetName: String? = null, - /** - * How many installable assets were offered to the user when they - * picked. Single-asset releases (count == 1) skip variant memory - * entirely because there's nothing to pin. - */ + pickedAssetSiblingCount: Int = 0, - /** - * Direct variant tag (already extracted) — takes precedence over - * the [pickedAssetName] derivation. Used by the import path - * where we already have the tag from a previous export rather - * than a fresh asset filename to extract from. - */ + preferredAssetVariant: String? = null, - /** - * Pre-derived multi-layer fingerprint from a previous export. - * Takes precedence over deriving from [pickedAssetName] when - * non-null — preserves the exact identity layers the user - * pinned in their other install rather than recomputing from - * a possibly-different asset list. - */ + preferredAssetTokens: String? = null, assetGlobPattern: String? = null, - /** - * Zero-based index of the picked asset in the release's - * installable-asset list. Stored for the same-position fallback - * — when the resolver can't match any fingerprint layer in a - * fresh release but the new release has the same number of - * installable assets, this index is preferred. - */ + pickedAssetIndex: Int? = null, sourceHost: String? = null, ) diff --git a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt index 5b2fed6ff..49aaf699f 100644 --- a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt +++ b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt @@ -56,12 +56,7 @@ private fun resolveInstalledIcon( .toBitmap() .asImageBitmap() } catch (t: Throwable) { - // Wide catch: NameNotFoundException is the common case but - // PackageManager can also throw SecurityException on cross-user - // reads, plus toBitmap() can throw if the drawable can't be - // rasterized (unsupported drawable type, OOM on huge icons). - // Composition crash on the Apps screen is the worst-case - // outcome — silently fall back to the default icon instead. + Log.w(TAG, "failed to load installed icon for $packageName", t) null } @@ -71,10 +66,7 @@ private fun resolveApkIcon( apkFilePath: String, ): ImageBitmap? = try { - // PackageManager.getApplicationIcon(applicationInfo) needs sourceDir - // to point at the APK so loadIcon() resolves the embedded drawable. - // Without setting sourceDir/publicSourceDir loadIcon() returns the - // default Android boilerplate icon — useless as a fallback. + val info = getPackageArchiveInfoCompat(packageManager, apkFilePath) val appInfo = info?.applicationInfo ?: return null appInfo.sourceDir = apkFilePath diff --git a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/PackageVisibilityRequester.android.kt b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/PackageVisibilityRequester.android.kt index 1bd88316c..f4bade984 100644 --- a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/PackageVisibilityRequester.android.kt +++ b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/PackageVisibilityRequester.android.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -// keep in sync with AndroidExternalAppScanner.GRANT_THRESHOLD private const val GRANT_THRESHOLD = 30 @Composable @@ -20,8 +19,7 @@ actual fun rememberPackageVisibilityRequester(): PackageVisibilityRequester { private class AndroidPackageVisibilityRequester( private val context: Context, ) : PackageVisibilityRequester { - // pm.getInstalledPackages is a binder IPC and can take noticeable time on devices - // with many packages — keep it off the main thread. + override suspend fun isGranted(): Boolean = withContext(Dispatchers.IO) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return@withContext true @@ -31,14 +29,7 @@ private class AndroidPackageVisibilityRequester( } override suspend fun requestOrOpenSettings(): Boolean { - // No-op by contract. QUERY_ALL_PACKAGES is granted at install time - // via the manifest declaration — there is no user-grantable runtime - // toggle on stock Android, and `ACTION_APPLICATION_DETAILS_SETTINGS` - // does not surface the (non-existent) toggle either. Some OEMs - // (Samsung One UI 4+) expose a "Special access → Allow access to all - // apps" setting, but no public Intent reliably deep-links to it. - // Callers must rely on `isGranted()` and gracefully degrade when - // false; the scanner's heuristic-based degraded path handles this. + return false } } diff --git a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.android.kt b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.android.kt index d2e071c65..4c10bc96e 100644 --- a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.android.kt +++ b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.android.kt @@ -8,10 +8,7 @@ import androidx.compose.ui.platform.LocalContext @Composable actual fun rememberSystemReducedMotion(): Boolean { val context = LocalContext.current - // Reading once at composition is fine — ANIMATOR_DURATION_SCALE is a - // global system setting that almost never flips while the wizard is - // on screen. If it does, the next composition (rotation, navigation) - // will pick up the new value. + return remember(context) { val scale = Settings.Global.getFloat( context.contentResolver, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt index 3bae4a3ee..3a938496b 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt @@ -5,7 +5,6 @@ import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.DeviceAppUi import zed.rainxch.apps.presentation.model.GithubAssetUi - sealed interface AppsAction { data object OnNavigateBackClick : AppsAction @@ -37,13 +36,6 @@ sealed interface AppsAction { data object OnRefresh : AppsAction - // Fired by AppsRoot on ON_RESUME. Routes through the existing - // cooldown-gated refresh path (`autoCheckForUpdatesIfNeeded`) so - // re-entering the Apps screen catches external installs that - // `PackageEventReceiver` may have missed — e.g. when GHS was - // background-killed by an aggressive OEM ROM and never received - // the PACKAGE_REPLACED broadcast. The 30-minute cooldown keeps - // rapid resumes from burning GitHub rate limits. data object OnLifecycleResume : AppsAction data object OnToggleUpToDateSection : AppsAction @@ -59,11 +51,9 @@ sealed interface AppsAction { val app: InstalledAppUi, ) : AppsAction - // Uninstall confirmation data class OnUninstallConfirmed(val app: InstalledAppUi) : AppsAction data object OnDismissUninstallDialog : AppsAction - // Link app to repo data object OnAddByLinkClick : AppsAction data object OnDismissLinkSheet : AppsAction data class OnDeviceAppSearchChange(val query: String) : AppsAction @@ -71,10 +61,7 @@ sealed interface AppsAction { data class OnLinkSuggestionSelected( val owner: String, val repo: String, - // Non-null when the suggestion came from a Forgejo / Codeberg / - // custom-forge search. Drives the URL the VM hands to the - // validate-and-link path so we don't navigate to github.com for - // a Forgejo repo. + val sourceHost: String? = null, ) : AppsAction data object OnLinkEnterUrlManually : AppsAction @@ -86,30 +73,18 @@ sealed interface AppsAction { data class OnLinkAssetSelected(val asset: GithubAssetUi) : AppsAction data object OnBackToEnterUrl : AppsAction - /** Asset filter input on the link-sheet PickAsset step. */ data class OnLinkAssetFilterChanged(val filter: String) : AppsAction - /** Toggle for "fall back to older releases" on the link-sheet PickAsset step. */ data class OnLinkFallbackToggled(val enabled: Boolean) : AppsAction - // Per-app pre-release toggle data class OnTogglePreReleases(val packageName: String, val enabled: Boolean) : AppsAction - // Per-app update-check toggle (issue #536: ignore updates for individual apps) data class OnToggleUpdateCheck(val packageName: String, val enabled: Boolean) : AppsAction - // Per-app skip-this-version (issue #542: dismiss false-positive update prompts). - // [tag] is the upstream release tag the user is choosing to skip — the - // ViewModel persists it on the row and the periodic check suppresses the - // badge until a strictly newer release lands. data class OnSkipReleaseTag(val packageName: String, val tag: String) : AppsAction - // Clears the per-app skip; the next update check will surface the badge - // again if a release matching the user's filters is still ahead of the - // installed version. data class OnUnskipReleaseTag(val packageName: String) : AppsAction - // Per-app advanced settings sheet (monorepo) data class OnOpenAdvancedSettings(val app: InstalledAppUi) : AppsAction data object OnDismissAdvancedSettings : AppsAction data class OnAdvancedFilterChanged(val filter: String) : AppsAction @@ -118,7 +93,6 @@ sealed interface AppsAction { data object OnAdvancedClearFilter : AppsAction data object OnAdvancedRefreshPreview : AppsAction - // Variant picker dialog (preferred APK variant) data class OnOpenVariantPicker( val app: InstalledAppUi, val resumeUpdateAfterPick: Boolean = false, @@ -127,7 +101,6 @@ sealed interface AppsAction { data class OnVariantSelected(val variant: String?) : AppsAction data object OnResetVariantToAuto : AppsAction - // Export/Import data object OnExportApps : AppsAction data object OnExportObtainium : AppsAction data object OnImportApps : AppsAction @@ -136,45 +109,24 @@ sealed interface AppsAction { data object OnDismissKaoBanner : AppsAction data object OnKaoLearnMore : AppsAction - /** - * User tapped the "Install" affordance on a row whose download - * was previously deferred (the user navigated away from Details - * mid-download). The orchestrator parked the file; this action - * picks it up and runs the installer. - */ data class OnInstallPendingApp( val app: InstalledAppUi, ) : AppsAction - /** - * User tapped the Discard affordance on a pending row — opens the - * confirmation dialog without doing anything destructive yet. The - * actual cleanup runs only after [OnConfirmDiscardPendingInstall]. - */ data class OnDiscardPendingInstall( val app: InstalledAppUi, ) : AppsAction - /** - * User confirmed the Discard prompt. Cancels the orchestrator - * entry, deletes the parked APK file, and removes the DB row so - * the app stops appearing in the list. - */ data class OnConfirmDiscardPendingInstall( val app: InstalledAppUi, ) : AppsAction - /** User dismissed the Discard confirmation without confirming. */ data object OnDismissDiscardPendingDialog : AppsAction - // External import banner (E1) data object OnImportProposalReview : AppsAction data object OnImportProposalDismiss : AppsAction - // Manual rescan trigger from the apps screen overflow. Resets the banner - // dismiss watermark and routes the user into the import wizard, which - // runs a fresh scan + match resolution on entry. data object OnRescanForGithubApps : AppsAction data object OnAddFromStarredClick : AppsAction diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsEvent.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsEvent.kt index 1306b6909..ce456b134 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsEvent.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsEvent.kt @@ -14,11 +14,7 @@ sealed interface AppsEvent { data class NavigateToRepo( val repoId: Long, val sourceHost: String? = null, - // Forgejo / Codeberg repos use a synthetic 64-bit repoId - // (RepoIdCodec) that the backend / GitHub APIs don't recognise. - // When sourceHost is set we have to look the repo up by - // owner+name instead. Carry them through nav so the VM doesn't - // fall back to GitHub `/repositories/{id}` and 404. + val owner: String? = null, val repo: String? = null, ) : AppsEvent diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index dd929c9ce..73041b5b0 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -175,11 +175,6 @@ fun AppsRoot( val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() - // Re-entry sync: fires the cooldown-gated update check whenever the - // user returns to the Apps screen. Catches external installs that - // `PackageEventReceiver` missed when GHS was background-killed by - // an aggressive OEM ROM. The VM debounces network calls via - // `UPDATE_CHECK_COOLDOWN_MS` (30 min) so rapid resumes are cheap. val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { val observer = @@ -210,10 +205,10 @@ fun AppsRoot( } } - is AppsEvent.AppLinkedSuccessfully -> { // handled by ShowSuccess + is AppsEvent.AppLinkedSuccessfully -> { } - is AppsEvent.ImportComplete -> { // handled by ShowSuccess + is AppsEvent.ImportComplete -> { } AppsEvent.NavigateToExternalImport -> { @@ -402,7 +397,6 @@ fun AppsScreen( }, ) { innerPadding -> - // Link app bottom sheet if (state.showLinkSheet) { LinkAppBottomSheet( state = state, @@ -410,7 +404,6 @@ fun AppsScreen( ) } - // Per-app advanced settings (monorepo filter / fallback) if (state.advancedSettingsApp != null) { AdvancedAppSettingsBottomSheet( state = state, @@ -418,7 +411,6 @@ fun AppsScreen( ) } - // Variant picker dialog (shown for stale variants or explicit picks) if (state.variantPickerApp != null) { VariantPickerDialog( state = state, @@ -426,7 +418,6 @@ fun AppsScreen( ) } - // Import summary sheet state.importSummary?.let { summary -> zed.rainxch.apps.presentation.components.ImportSummarySheet( summary = summary, @@ -434,7 +425,6 @@ fun AppsScreen( ) } - // Uninstall confirmation dialog state.appPendingUninstall?.let { app -> AlertDialog( onDismissRequest = { onAction(AppsAction.OnDismissUninstallDialog) }, @@ -469,10 +459,6 @@ fun AppsScreen( ) } - // Discard-pending-install confirmation dialog. Mirrors the - // uninstall flow because both branches blow away DB rows the - // user might still want — the discard target also deletes the - // parked APK from disk, so the prompt is doubly warranted. state.appPendingDiscard?.let { app -> AlertDialog( onDismissRequest = { onAction(AppsAction.OnDismissDiscardPendingDialog) }, @@ -634,9 +620,6 @@ fun AppsScreen( val listState = rememberLazyListState() val isScrollbarEnabled = LocalScrollbarEnabled.current - // Split filteredApps into the "Updates available" group (rich - // rows) and the "Up to date" group (compact rows) — issue #463. - // Sort order is already applied by the ViewModel. val updatesGroup = state.filteredApps.filter { it.installedApp.isUpdateAvailable && it.installedApp.updateCheckEnabled @@ -654,10 +637,7 @@ fun AppsScreen( LazyColumn( state = listState, modifier = Modifier.fillMaxSize().arrowKeyScroll(listState), - // Bottom inset clears the Add-by-link FAB so - // the last list item isn't hidden under it. - // FAB ≈ 56dp + 16dp scaffold inset + 16dp - // breathing room. + contentPadding = PaddingValues( start = 0.dp, end = 0.dp, @@ -1009,9 +989,7 @@ fun AppItemCard( } when { - // Highest priority: a download finished while - // the user wasn't watching, the file is on disk - // and ready to be installed with one tap. + app.pendingInstallFilePath != null -> { Text( text = stringResource(Res.string.ready_to_install), @@ -1030,10 +1008,7 @@ fun AppItemCard( } app.preferredVariantStale -> { - // Tap-to-fix label: route through the same OnUpdateApp - // intercept that would have opened the picker anyway, - // but also surface a tappable hint here for users - // who notice the warning before tapping Update. + Text( text = stringResource(Res.string.variant_stale_hint), style = MaterialTheme.typography.bodySmall, @@ -1060,9 +1035,7 @@ fun AppItemCard( maxLines = 2, overflow = TextOverflow.Ellipsis, ) - // Show the pinned variant tag inline so users can - // see at a glance which APK they'll get when they - // tap Update. + if (!app.preferredAssetVariant.isNullOrBlank()) { Text( text = @@ -1125,10 +1098,7 @@ fun AppItemCard( Row( verticalAlignment = Alignment.CenterVertically, ) { - // Subtle visual cue when a monorepo filter is active — - // the icon tints to primary, so users can tell at a - // glance which apps have an active filter without - // having to open the sheet. + val advancedFilterDescription = stringResource(Res.string.advanced_settings_open) val hasFilter = @@ -1152,10 +1122,6 @@ fun AppItemCard( ) } - // Always-visible "Pick variant" entry point. Tints to - // primary when a variant is pinned (so users can see - // at a glance whether the app has a sticky pick) and - // to error when the pinned variant has gone stale. val pickVariantDescription = stringResource(Res.string.variant_picker_open) val hasPin = !app.preferredAssetVariant.isNullOrBlank() @@ -1220,12 +1186,6 @@ fun AppItemCard( }, ) - // Skip-this-version is only meaningful when an - // update is currently being prompted (we have a - // latestVersion to skip). When the row already - // has a skipped tag stored, surface the unskip - // affordance instead so the user can revert the - // suppression without leaving the row. if (app.skippedReleaseTag != null) { DropdownMenuItem( text = { Text(stringResource(Res.string.apps_skip_version_unskip)) }, @@ -1383,9 +1343,7 @@ fun AppItemCard( else -> { if (app.pendingInstallFilePath != null) { - // One-tap install for a deferred download. - // Bypasses the download phase entirely — - // the file is already on disk. + Button( onClick = onInstallPendingClick, modifier = Modifier.weight(1f), @@ -1401,9 +1359,7 @@ fun AppItemCard( text = stringResource(Res.string.install), ) } - // Quick escape hatch: user cancelled the - // system prompt and doesn't want this app. - // Discard removes the parked file + DB row. + IconButton(onClick = onDiscardPendingClick) { Icon( imageVector = Icons.Default.Cancel, @@ -1427,12 +1383,7 @@ fun AppItemCard( ) } } else if (app.isPendingInstall) { - // Pending row whose parked file is gone - // (legacy row from before we persisted the - // path, or file deleted out-of-band). The - // app isn't actually installed — Open would - // snackbar an error. Offer Discard so the - // user can clear the row. + Button( onClick = onDiscardPendingClick, modifier = Modifier.weight(1f), diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt index c53cf535e..274004093 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt @@ -27,13 +27,9 @@ data class AppsState( val isCheckingForUpdates: Boolean = false, val lastCheckedTimestamp: Long? = null, val isRefreshing: Boolean = false, - /** - * Whether the "Up to date" section is expanded. Default expanded so - * users with no updates pending still see their apps. Collapses - * independently of the (always-expanded) "Updates available" section. - */ + val isUpToDateSectionExpanded: Boolean = true, - // Link app to repo + val showLinkSheet: Boolean = false, val linkStep: LinkStep = LinkStep.PickApp, val deviceApps: ImmutableList = persistentListOf(), @@ -50,13 +46,13 @@ data class AppsState( val linkSelectedAsset: GithubAssetUi? = null, val linkDownloadProgress: Int? = null, val fetchedRepoInfo: GithubRepoInfoUi? = null, - /** Filter input on the PickAsset step. Live-narrows [linkInstallableAssets]. */ + val linkAssetFilter: String = "", - /** Validation message for [linkAssetFilter] (invalid regex syntax). */ + val linkAssetFilterError: String? = null, - /** Whether linking should also enable fallback-to-older-releases. */ + val linkFallbackToOlder: Boolean = false, - // Per-app advanced settings (monorepo support) + val advancedSettingsApp: InstalledAppUi? = null, val advancedFilterDraft: String = "", val advancedFallbackDraft: Boolean = false, @@ -66,33 +62,27 @@ data class AppsState( val advancedPreviewTag: String? = null, val advancedPreviewMessage: String? = null, val advancedSavingFilter: Boolean = false, - // Variant picker dialog (shown when preferredVariantStale, when the - // user explicitly opens it from advanced settings, or when they tap - // Update on a stale-variant app) + val variantPickerApp: InstalledAppUi? = null, val variantPickerLoading: Boolean = false, val variantPickerOptions: ImmutableList = persistentListOf(), val variantPickerCurrentVariant: String? = null, val variantPickerError: String? = null, - /** - * Set when the picker is being shown specifically because the user - * tapped Update on a stale-variant app — after they pick we should - * automatically resume the update flow. - */ + val variantPickerResumeUpdateAfterPick: Boolean = false, - // Export/Import + val isExporting: Boolean = false, val isImporting: Boolean = false, val importSummary: zed.rainxch.apps.domain.model.ImportResult? = null, - // Uninstall confirmation + val appPendingUninstall: InstalledAppUi? = null, - // Discard-pending-install confirmation + val appPendingDiscard: InstalledAppUi? = null, - // External import banner (E1) + val pendingExternalImportCount: Int = 0, val showImportProposalBanner: Boolean = false, val isExternalImportInFlight: Boolean = false, - // Keep Android Open campaign banner + val showKaoBanner: Boolean = false, val linkSourceHost: String? = null, ) { @@ -115,12 +105,6 @@ data class AppsState( ).toImmutableList() } - /** - * Live-filtered view of [linkInstallableAssets] for the link sheet's - * PickAsset step. When the filter is invalid we keep showing the full - * list so the user can still pick something — the error is surfaced via - * [linkAssetFilterError]. - */ val filteredLinkAssets: ImmutableList get() { val raw = linkAssetFilter.trim() diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index f747ff239..143fd06b9 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -77,15 +77,9 @@ class AppsViewModel( private var updateAllJob: Job? = null private var lastAutoCheckTimestamp: Long = 0L - // Synchronous mirror of the persisted dismiss watermark. The persisted - // write goes through DataStore async, so the import-banner flow could - // re-emit with the OLD watermark and re-flash the banner before the - // write completes. This in-memory shadow is set BEFORE the launch and - // OR'd into shouldShow so the suppression is immediate. @Volatile private var localBannerDismissedAtCount: Int = 0 - /** Debounced re-runs of the live preview in the advanced settings sheet. */ private var advancedPreviewJob: Job? = null private val _state = MutableStateFlow(AppsState()) @@ -119,10 +113,7 @@ class AppsViewModel( count to dismissedAt } .collect { (count, dismissedAt) -> - // The effective watermark is the max of the persisted value and the - // synchronous local one. The local shadow is set immediately by the - // Review/Dismiss handlers so a flow emission that beats the DataStore - // write still sees a watermark that suppresses the banner. + val effectiveDismissedAt = maxOf(dismissedAt, localBannerDismissedAtCount) val shouldShow = count >= BANNER_THRESHOLD && count > effectiveDismissedAt _state.update { @@ -207,12 +198,7 @@ class AppsViewModel( private fun checkAllForUpdates() { viewModelScope.launch { - // Stamp the inflight guard at launch start — a second resume - // that races the network call finds the cooldown gate already - // tripped and exits early. `lastCheckedTimestamp` (the UI - // "last checked" indicator) still only advances on success so - // a failed check doesn't lie to the user — matches the - // `refresh()` semantics. + lastAutoCheckTimestamp = System.currentTimeMillis() _state.update { it.copy(isCheckingForUpdates = true) } try { @@ -289,10 +275,7 @@ class AppsViewModel( } is AppsAction.OnUpdateApp -> { - // If the app's pinned variant has gone missing in the - // latest release we don't know what to download — open - // the picker first and resume the update after the user - // chooses. Saves them from a wrong-variant install. + if (action.app.preferredVariantStale) { openVariantPicker(action.app, resumeUpdateAfterPick = true) } else { @@ -607,10 +590,7 @@ class AppsViewModel( AppsAction.OnImportProposalReview -> { val current = _state.value.pendingExternalImportCount - // Set the local watermark BEFORE the launch so a racing flow - // emission can't recompute shouldShow=true on the stale persisted - // watermark. observePendingExternalImports OR's this with the - // persisted value via maxOf(). + localBannerDismissedAtCount = maxOf(localBannerDismissedAtCount, current) _state.update { it.copy(showImportProposalBanner = false) } viewModelScope.launch { @@ -629,9 +609,7 @@ class AppsViewModel( } AppsAction.OnRescanForGithubApps -> { - // Manual rescan resets the banner watermark so the user sees - // everything fresh; the wizard's startScanIfIdle then runs - // a full scan + match cycle on entry. + localBannerDismissedAtCount = 0 _state.update { it.copy(showImportProposalBanner = false) } viewModelScope.launch { @@ -771,11 +749,6 @@ class AppsViewModel( if (errorKey == null) schedulePreviewRefresh() } - /** - * Debounces preview refresh while the user is typing. We don't want to - * issue a fresh GitHub releases call on every keystroke — 350ms after - * input stops is plenty responsive without burning rate limit. - */ private fun schedulePreviewRefresh() { advancedPreviewJob?.cancel() advancedPreviewJob = @@ -790,8 +763,6 @@ class AppsViewModel( val draftFilter = _state.value.advancedFilterDraft val draftFallback = _state.value.advancedFallbackDraft - // Validate locally before hitting the network — invalid regex - // shows the error inline and aborts the preview. val parseResult = AssetFilter.parse(draftFilter) if (parseResult != null && parseResult.isFailure) { _state.update { @@ -853,13 +824,6 @@ class AppsViewModel( } } - /** - * Opens the variant picker for [app]. Fetches the current latest - * matching release (honouring the per-app filter / fallback) so the - * dialog can show real, current asset names — not the cached ones - * which might be stale or wrong. When [resumeUpdateAfterPick] is - * true, dispatch the update flow as soon as the user picks. - */ private fun openVariantPicker( app: InstalledAppUi, resumeUpdateAfterPick: Boolean, @@ -884,11 +848,7 @@ class AppsViewModel( includePreReleases = app.includePreReleases, fallbackToOlderReleases = app.fallbackToOlderReleases, ) - // Only assets whose filename has an extractable, non-empty - // variant tag are pinnable: an empty extract or null means - // we'd have nothing to remember release-over-release. The - // dialog filters its own list so users can't tap a row - // that would silently no-op. + val pinnableAssets = preview.matchedAssets.filter { asset -> AssetVariant.extract(asset.name)?.isNotEmpty() == true @@ -922,12 +882,6 @@ class AppsViewModel( } } - /** - * Persists the user's variant pick (or null to reset to auto), - * dismisses the dialog, and — if the picker was opened from a "tap - * Update on stale variant" flow — kicks the update off automatically - * with the freshly-resolved cached fields. - */ private fun saveVariantSelection(variant: String?) { val app = _state.value.variantPickerApp ?: return val resume = _state.value.variantPickerResumeUpdateAfterPick @@ -946,7 +900,6 @@ class AppsViewModel( return@launch } - // Dismiss the dialog regardless of whether we resume. _state.update { it.copy( variantPickerApp = null, @@ -959,15 +912,7 @@ class AppsViewModel( } if (resume) { - // Read the canonical InstalledApp directly from the - // repository rather than the in-memory state. The Flow - // that drives `_state.value.apps` propagates DAO writes - // asynchronously, so reading state right after - // setPreferredVariant — which itself runs an internal - // checkForUpdates write — can race and hand us the OLD - // pre-pick row, leading to an update with the wrong - // asset URL. A direct DAO read is synchronous and never - // races against pending Flow emissions. + val refreshed = runCatching { installedAppsRepository.getAppByPackage(app.packageName) } .getOrNull() @@ -984,7 +929,6 @@ class AppsViewModel( val draftFilter = _state.value.advancedFilterDraft.trim() val draftFallback = _state.value.advancedFallbackDraft - // Final regex validation — if it's broken we refuse to save. val parseResult = AssetFilter.parse(draftFilter) if (parseResult != null && parseResult.isFailure) { _state.update { it.copy(advancedFilterError = "invalid") } @@ -994,8 +938,7 @@ class AppsViewModel( viewModelScope.launch { _state.update { it.copy(advancedSavingFilter = true) } try { - // `setAssetFilter` persists and then re-checks internally, - // so the UI badge is refreshed without a second round-trip. + installedAppsRepository.setAssetFilter( packageName = app.packageName, regex = draftFilter.takeIf { it.isNotEmpty() }, @@ -1112,13 +1055,6 @@ class AppsViewModel( throw IllegalStateException("No installable assets found for this platform") } - // Honour the user's pinned variant first; fall back to - // the platform installer's auto-pick if the variant - // isn't present in this release. The auto-pick - // intentionally never throws here — checkForUpdates - // already flipped `preferredVariantStale=true` and the - // earlier intercept (see updateSingleApp entrypoint) - // would have routed us to the picker dialog instead. val variantMatch = AssetVariant.resolvePreferredAsset( assets = installableAssets, @@ -1166,13 +1102,6 @@ class AppsViewModel( updateAppState(app.packageName, UpdateState.Downloading) - // Route the download through the orchestrator so - // it survives this VM being torn down (user - // navigating away from the apps tab). Shizuku - // gets AlwaysInstall (silent install regardless - // of foreground state); regular installer gets - // InstallWhileForeground so the existing dialog/ - // installer dispatch below stays in charge. val installerType = try { tweaksRepository.getInstallerType().first() @@ -1254,10 +1183,6 @@ class AppsViewModel( throw e } - // Successful install — release the orchestrator - // entry so the apps row stops showing the - // download/install state. The DB sync continues - // via PackageEventReceiver. downloadOrchestrator.dismiss(app.packageName) try { installedAppsRepository.setPendingInstallFilePath(app.packageName, null) @@ -1471,10 +1396,7 @@ class AppsViewModel( packageName: String, progress: Int?, ) { - // Download progress is purely a per-row visual update; it doesn't - // affect sort order or search match. Avoid re-running the full - // filter+sort pass on every progress tick (which at 50+ apps with - // a download in flight ran ~100×/sec). Map both lists in place. + _state.update { currentState -> currentState.copy( apps = @@ -1504,30 +1426,6 @@ class AppsViewModel( logger.debug("Marked ${app.packageName} as pending install") } - /** - * Subscribes to the orchestrator's entry for [packageName] and - * suspends until it reaches a terminal stage. Mirrors progress - * via [onProgress] while downloading. - * - * Returns: - * - The file path when the orchestrator parks the file at - * [OrchestratorStage.AwaitingInstall] (regular installer path) - * - `null` when the orchestrator finishes its own install - * ([OrchestratorStage.Completed], the Shizuku/AlwaysInstall - * path) — the caller has nothing more to do - * - `null` when the entry is cancelled or fails — the caller - * treats this as "abort the local install logic" - * - * Implementation: forwards progress side-effects via a - * `transform` step, then `first { predicate }` finds the first - * emission whose stage is terminal. Avoids needing to throw out - * of `collect`. - */ - /** - * Result type for [waitForOrchestratorReady] so callers can - * distinguish "file is ready" from "orchestrator already installed" - * from "download failed". - */ private sealed interface OrchestratorResult { data class Ready(val filePath: String) : OrchestratorResult data object AlreadyInstalled : OrchestratorResult @@ -1565,16 +1463,6 @@ class AppsViewModel( } } - /** - * Triggers an install for an app whose download was previously - * deferred (the orchestrator parked the file in `AwaitingInstall` - * mode after the user navigated away mid-download). Used by the - * apps row "Install" button when [InstalledAppUi.pendingInstallFilePath] - * is non-null. - * - * Delegates to [DownloadOrchestrator.installPending] which - * handles validation-free install + DB cleanup. - */ private fun installPendingApp(app: InstalledAppUi) { if (activeUpdates.containsKey(app.packageName)) { logger.debug("Install already in progress for ${app.packageName}") @@ -1597,13 +1485,6 @@ class AppsViewModel( } } - /** - * User decided to drop a pending install — they cancelled the - * system installer prompt and don't want this app anymore. Cancels - * the orchestrator entry (this also nukes the parked APK file via - * the downloader's cancel path), removes the DB row, and clears - * any leftover pending notification. - */ private fun discardPendingInstall(app: InstalledAppUi) { viewModelScope.launch { try { @@ -1613,11 +1494,7 @@ class AppsViewModel( } catch (t: Throwable) { logger.warn("discardPendingInstall: orchestrator cancel failed: ${t.message}") } - // Belt-and-braces: cancel doesn't nuke the file when the - // entry was already in AwaitingInstall — the orchestrator - // already cleared its in-memory metadata before the user - // tapped Discard. Delete the parked file directly so the - // bytes don't leak. + app.pendingInstallFilePath?.let { path -> runCatching { File(path).takeIf { it.exists() }?.delete() } .onFailure { @@ -1631,11 +1508,7 @@ class AppsViewModel( } catch (e: CancellationException) { throw e } catch (t: Throwable) { - // Mirror uninstallApp's UX: a row-delete failure leaves - // the apps list pointing at metadata for an APK that's - // already been removed from disk, so the user has to - // know it didn't take. Log the cause and surface a - // localized error event for the snackbar host. + logger.error( "discardPendingInstall: row delete failed for ${app.packageName}: ${t.message}", ) @@ -1787,15 +1660,11 @@ class AppsViewModel( } private fun onLinkAssetFilterChanged(value: String) { - // Validate the regex on every keystroke so the user gets immediate - // feedback. The state's filteredLinkAssets getter falls back to the - // unfiltered list when the regex is invalid, so the picker stays - // usable even mid-typing. + val parseResult = AssetFilter.parse(value) val error = parseResult?.exceptionOrNull()?.let { _ -> - // Localized message comes from the UI layer; here we just - // signal that something is wrong. + "invalid" } _state.update { @@ -1806,22 +1675,6 @@ class AppsViewModel( } } - /** - * Picks a sensible default for the link-flow filter. Tries, in order: - * 1. The trailing segment of the package name (e.g. `io.ente.auth` → `auth`) - * 2. A token derived from the device app's display name (e.g. - * `Ente Auth` → `auth`) - * 3. [AssetFilter.suggestFromAssetName] on the first asset - * - * Every candidate is routed through [Regex.escape] before validation - * so metacharacters in package names or display words (think - * `My App (Beta)` → `(beta)`) are treated literally and never break - * regex compilation. - * - * Returns the first non-blank candidate that actually matches at least - * one of the available assets — otherwise null, which leaves the field - * empty so we don't pre-fill something useless. - */ private fun suggestFilterForLink( deviceAppName: String, packageName: String, @@ -1839,11 +1692,9 @@ class AppsViewModel( return if (assets.any { regex.containsMatchIn(it.name) }) escaped else null } - // 1. Last package segment (commonly the most distinctive token). val packageTail = packageName.substringAfterLast('.').lowercase() tryCandidate(packageTail)?.let { return it } - // 2. Significant words from the display name. deviceAppName .split(' ', '-', '_') .map { it.lowercase().trim() } @@ -1851,8 +1702,6 @@ class AppsViewModel( tryCandidate(token)?.let { return it } } - // 3. Heuristic on the first asset name (already escaped + anchored - // by AssetFilter.suggestFromAssetName). return firstAssetName?.let { AssetFilter.suggestFromAssetName(it) } } @@ -1969,11 +1818,6 @@ class AppsViewModel( return@launch } - // Seed an auto-suggestion based on the device app's package - // name first, then fall back to the first installable asset. - // This makes monorepo linking nearly zero-effort: pick "Ente - // Auth" → the filter pre-fills with "auth" so the picker - // already shows just the relevant APKs. val suggestedFilter = suggestFilterForLink( deviceAppName = selectedApp.appName, @@ -2024,11 +1868,6 @@ class AppsViewModel( val assetFilterRegex = _state.value.linkAssetFilter.takeIf { it.isNotBlank() } val fallbackToOlder = _state.value.linkFallbackToOlder - // The user explicitly picked this asset → trust the choice. - // Skip the re-download + signing-key verification dance (which - // burned bandwidth and time for nothing the user couldn't already - // confirm). The `installSource = MANUAL` field linkAppToRepo - // writes is what the Details banner reads to flag manual links. viewModelScope.launch { _state.update { it.copy( @@ -2174,11 +2013,6 @@ class AppsViewModel( override fun onCleared() { super.onCleared() - // Cancel local OBSERVERS only — the orchestrator entries - // keep running in the application scope. Each in-flight - // download is downgraded to DeferUntilUserAction so the - // user gets a notification when it's ready, instead of - // losing the work. updateAllJob?.cancel() val packageNames = activeUpdates.keys.toList() activeUpdates.values.forEach { it.cancel() } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt index 470a8574d..4b477a235 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt @@ -51,15 +51,6 @@ import zed.rainxch.apps.presentation.AppsState import zed.rainxch.apps.presentation.model.GithubAssetUi import zed.rainxch.githubstore.core.presentation.res.* -/** - * Per-app advanced settings sheet for monorepo support. Shows: - * - Asset filter (regex) text field with inline validation - * - Fall-back-to-older-releases toggle - * - **Live preview** of which assets in the latest matching release the - * current draft would resolve to. This is the killer UX touch — users - * can iterate on the regex and immediately see the effect without - * having to save and run an update check. - */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AdvancedAppSettingsBottomSheet( @@ -112,7 +103,6 @@ fun AdvancedAppSettingsBottomSheet( Spacer(Modifier.height(20.dp)) - // === Asset filter === OutlinedTextField( value = state.advancedFilterDraft, onValueChange = { onAction(AppsAction.OnAdvancedFilterChanged(it)) }, @@ -153,7 +143,6 @@ fun AdvancedAppSettingsBottomSheet( Spacer(Modifier.height(12.dp)) - // === Fallback toggle === Row( modifier = Modifier .fillMaxWidth() @@ -185,17 +174,6 @@ fun AdvancedAppSettingsBottomSheet( ) Spacer(Modifier.height(16.dp)) - // === Preferred variant row === - // Tappable row that opens the variant picker dialog. Shows - // the currently-pinned variant tag (or "Auto" when none), - // and warns the user when the pin has gone stale. - // - // Cross-link copy: a one-liner above the row clarifies - // the *relationship* between the filter (which assets are - // even considered) and the variant pin (which of the - // matching assets gets installed) — these are the two - // axes a user is actually adjusting when they wonder why - // an update grabbed the wrong file. Text( text = stringResource(Res.string.advanced_filter_variant_relation), style = MaterialTheme.typography.bodySmall, @@ -214,7 +192,6 @@ fun AdvancedAppSettingsBottomSheet( ) Spacer(Modifier.height(16.dp)) - // === Live preview === PreviewSection( isLoading = state.advancedPreviewLoading, matchedAssets = state.advancedPreviewMatched, @@ -225,7 +202,6 @@ fun AdvancedAppSettingsBottomSheet( Spacer(Modifier.height(20.dp)) - // === Save / cancel buttons === Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt index 346b0cce5..89c79a260 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt @@ -34,15 +34,6 @@ import zed.rainxch.githubstore.core.presentation.res.apps_section_expand import zed.rainxch.githubstore.core.presentation.res.apps_section_state_collapsed import zed.rainxch.githubstore.core.presentation.res.apps_section_state_expanded -/** - * Section header shown above each grouped app list. Honours WCAG: the row - * carries `Role.Button` + `heading()` semantics, announces expanded / - * collapsed state via `stateDescription`, and includes the item count in - * the visible label so it's read as part of the heading. - * - * Pass [collapsible] = false to render a static heading (e.g. for the - * always-expanded "Updates available" group). - */ @Composable fun AppsSectionHeader( title: String, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt index ff1bbb5a9..115b88384 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt @@ -70,17 +70,6 @@ import zed.rainxch.githubstore.core.presentation.res.discard_pending_install import zed.rainxch.githubstore.core.presentation.res.variant_picker_open import kotlin.time.ExperimentalTime -/** - * 64dp single-line row used in the "Up to date" section. Drops the verbose - * controls of [zed.rainxch.apps.presentation.AppItemCard] (filter / variant / - * pre-release / inline status text). Per-app configuration moves into the - * trailing overflow menu, which routes to the existing bottom sheet. - * - * Accessibility: the entire row carries a single merged semantic name that - * surfaces every "hidden" flag (filter active / variant pinned / pre-release - * on / variant stale / pending install / ready to install) so screen-reader - * users don't lose context that the dot cluster encodes visually. - */ @Composable fun CompactAppRow( appItem: AppItem, @@ -159,9 +148,7 @@ fun CompactAppRow( } if (app.pendingInstallFilePath != null) { - // One-tap path for a parked download — surface the install - // primary CTA even in compact mode because the file is on disk - // and finishing the install is the user's expected action. + Button( onClick = onInstallPendingClick, enabled = !isBusy, @@ -181,13 +168,9 @@ fun CompactAppRow( } Spacer(Modifier.width(4.dp)) } else if (app.isPendingInstall) { - // Pending row whose file isn't (or is no longer) on disk. - // The app isn't installed; suppress the Open shortcut so we - // don't dead-end on a launch failure. Discard is reachable - // from the overflow. + } else if (!isBusy) { - // Subtle Open shortcut keeps the most-frequent action one tap - // away even though the row itself opens the repo on tap. + IconButton( onClick = onOpenClick, modifier = Modifier.size(40.dp), @@ -348,7 +331,6 @@ private fun CompactRowOverflow( } } } - // Suppress unused-parameter warning; isUpdateAvailable reserved for - // future Update CTA in compact mode if we ever want it. + @Suppress("UNUSED_EXPRESSION") isUpdateAvailable } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt index 497a72f7b..466291d4a 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt @@ -3,18 +3,6 @@ package zed.rainxch.apps.presentation.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -/** - * Renders the icon for a tracked app. - * - * Resolution order on Android: - * 1. The icon registered with the system PackageManager for [packageName]. - * 2. The icon embedded inside the parked APK at [apkFilePath] (used for - * pending-install rows whose package isn't on the system yet). - * 3. A generic fallback drawable. - * - * On JVM (desktop) the fallback is always used — there's no - * platform-resident icon registry to consult. - */ @Composable expect fun InstalledAppIcon( packageName: String, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt index 3efdd4a66..7fac63027 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt @@ -458,11 +458,7 @@ private fun SmartMatchStep( ) { items( items = suggestions, - // Lists may now mix GitHub + Forgejo hits for - // the same owner/repo slug across hosts — - // include sourceHost in the key so identical - // slugs on different forges render as distinct - // rows. + key = { "${it.sourceHost ?: "github"}|${it.owner}/${it.repo}" }, ) { suggestion -> SuggestionRow( @@ -530,7 +526,7 @@ private fun SuggestionRow( ) } Row(verticalAlignment = Alignment.CenterVertically) { - // Host badge — answers "where is this suggestion from". + HostBadge(suggestion.sourceHost) Spacer(Modifier.width(6.dp)) MatchSourceChip(suggestion.source) @@ -597,11 +593,7 @@ private fun MatchSourceChip(source: RepoMatchSource) { RepoMatchSource.FINGERPRINT -> stringResource(Res.string.match_source_fingerprint) RepoMatchSource.SEARCH -> stringResource(Res.string.match_source_search) RepoMatchSource.MANUAL -> stringResource(Res.string.match_source_manual) - // Reuses the "search" string for now — distinguishing the - // forge in the chip would need a new translated label per - // locale, which can come later. The underlying source-host - // is what actually drives URL building and the row's source - // chip on the Details / Apps screen. + RepoMatchSource.FORGEJO_SEARCH -> stringResource(Res.string.match_source_search) } Surface( @@ -655,7 +647,6 @@ private fun EnterUrlStep( Spacer(Modifier.height(16.dp)) - // Selected app info if (selectedApp != null) { Row( modifier = Modifier @@ -790,9 +781,6 @@ private fun PickAssetStep( Spacer(Modifier.height(12.dp)) - // Asset filter — for monorepos that ship multiple apps from the - // same repo. Live-narrows the visible list and is persisted with - // the link, so the update checker only ever resolves matching APKs. OutlinedTextField( value = filterValue, onValueChange = onFilterChanged, @@ -815,10 +803,7 @@ private fun PickAssetStep( visibleAssets.isEmpty() && filterValue.isNotBlank() -> stringResource(Res.string.asset_filter_no_match) filterValue.isNotBlank() -> - // Pass the total asset count as the plural - // quantity so Polish/Russian inflection picks - // the right form based on the *collection* - // size, and supply both counts as format args. + pluralStringResource( Res.plurals.asset_filter_visible_count, allAssets.size, @@ -841,9 +826,6 @@ private fun PickAssetStep( Spacer(Modifier.height(8.dp)) - // Fall-back-to-older-releases toggle. Only meaningful when a filter - // is set; in monorepos, the latest release is often for the wrong - // app, so the checker needs to walk back to find this app's APK. Row( modifier = Modifier .fillMaxWidth() @@ -938,12 +920,7 @@ private fun PickAssetStep( if (visibleAssets.isEmpty()) { item { - // Three distinct empty states: - // - No installable assets at all in the repo release - // (defensive: validateAndLinkRepo short-circuits - // this today, but guard in case flows change) - // - Filter regex is invalid (shown in error color) - // - Filter is valid but matched nothing + val (message, isError) = when { allAssets.isEmpty() -> stringResource(Res.string.asset_none_available) to false diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/StatusDotCluster.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/StatusDotCluster.kt index e252a4fdb..55646c13e 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/StatusDotCluster.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/StatusDotCluster.kt @@ -26,12 +26,6 @@ import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_updates import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_variant_pinned import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_variant_stale -/** - * Per-app state flags that the compact row encodes visually. Each flag uses - * BOTH a unique [DotShape] AND a tinted color so that the cluster passes - * WCAG 1.4.1 (no information by color alone). Screen readers see the - * cluster's merged contentDescription via the parent row's semantics. - */ data class CompactStatusFlags( val filterActive: Boolean = false, val variantPinned: Boolean = false, @@ -42,11 +36,6 @@ data class CompactStatusFlags( val updatesIgnored: Boolean = false, ) -/** - * Computes the visible status flags for an [AppItem]. Memoised on its - * inputs so that the row's recomposition doesn't re-allocate the data - * class on every download-progress tick. - */ @Composable fun rememberCompactStatusFlags(appItem: AppItem): CompactStatusFlags { val app = appItem.installedApp @@ -72,12 +61,6 @@ fun rememberCompactStatusFlags(appItem: AppItem): CompactStatusFlags { } } -/** - * Renders the active flags as a compact row of 8dp shapes. Each flag has a - * dedicated shape (circle / square / triangle / diamond / ring / chevron) so - * shape-only viewers (deuteranopia, monochrome themes, low contrast) still - * tell the flags apart. - */ @Composable fun StatusDotCluster( flags: CompactStatusFlags, @@ -172,11 +155,6 @@ private fun DrawScope.drawShape(shape: DotShape, color: Color) { } } -/** - * Builds the merged accessible name for the row. The name template is - * read once per row recomposition; the dot cluster itself carries no - * semantics (decorative). - */ @Composable fun buildCompactRowSemantics( appName: String, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt index 11d334add..339706602 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt @@ -41,18 +41,6 @@ import zed.rainxch.apps.presentation.AppsState import zed.rainxch.core.domain.util.AssetVariant import zed.rainxch.githubstore.core.presentation.res.* -/** - * Dialog for picking the preferred APK variant when an app's release - * has multiple installable assets. Opened from: - * - The advanced settings sheet (explicit user action) - * - Tapping Update on an app whose `preferredVariantStale` is true - * (the picker takes over so the user resolves the ambiguity before - * the wrong APK gets downloaded) - * - * Each option corresponds to a stable variant tag derived from the - * filename. There's also a "Reset to auto" entry that clears the - * preference and lets the platform installer's auto-picker do its job. - */ @Composable fun VariantPickerDialog( state: AppsState, @@ -155,11 +143,7 @@ fun VariantPickerDialog( } }, confirmButton = { - // Cross-link: jump to the asset filter sheet for this app. - // The two are conceptually adjacent — filter decides which - // assets are *considered*, the variant picker decides which - // of the matching assets gets installed. Users debugging - // "wrong file installed" need both within reach. + TextButton( onClick = { onAction(AppsAction.OnDismissVariantPicker) @@ -227,7 +211,7 @@ private fun VariantOptionList( .heightIn(min = 0.dp, max = 280.dp), verticalArrangement = Arrangement.spacedBy(2.dp), ) { - // Reset-to-auto entry — placed at the top so it's always discoverable. + item { VariantRow( isSelected = current == null, @@ -242,10 +226,7 @@ private fun VariantOptionList( } items(state.variantPickerOptions, key = { it.id }) { asset -> - // The ViewModel guarantees every asset reaching this list has - // a non-null, non-empty extract — see openVariantPicker's - // pinnableAssets filter. Treat null as a defensive fallback - // and skip the row to keep the dialog tappable everywhere. + val variant = AssetVariant.extract(asset.name) if (variant.isNullOrEmpty()) return@items val isCurrent = variant.equals(current, ignoreCase = true) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportAction.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportAction.kt index 0ffc25305..73e5aff1f 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportAction.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportAction.kt @@ -45,12 +45,5 @@ sealed interface ExternalImportAction { data object OnAddManually : ExternalImportAction - /** - * Cancels in-flight scan / auto-import work and drops to the review - * wizard with whatever candidates have already been resolved (or an - * empty Done state if nothing made it through yet). Exposed via the - * Skip button that surfaces after the progress screen has been up - * past the skip threshold (~5 s). - */ data object OnSkipLongScan : ExternalImportAction } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt index 0b82925c2..27cb553ad 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt @@ -75,9 +75,7 @@ fun ExternalImportRoot( } ExternalImportEvent.PlayConfetti -> confettiTrigger++ is ExternalImportEvent.ShowUndoSnackbar -> { - // Dismiss any prior snackbar so undo always wins the slot — the - // VM tracks one pending undo, and showing a stale message would - // let the user mis-target it. + snackbarHostState.currentSnackbarData?.dismiss() scope.launch { val undoLabel = getString(Res.string.external_import_undo_action) @@ -263,8 +261,6 @@ fun ExternalImportRoot( } } - // Confetti is gated on PlayConfetti events: each event bumps the trigger - // and remounts the overlay so its LaunchedEffect re-runs the burst. if (state.phase == ImportPhase.Done && confettiTrigger > 0) { androidx.compose.runtime.key(confettiTrigger) { ConfettiOverlay(enabled = true) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportState.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportState.kt index ebf6479bd..d388a0ba3 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportState.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportState.kt @@ -28,10 +28,7 @@ data class ExternalImportState( val invisiblePackageCountEstimate: Int = 0, val showCompletionToast: Boolean = false, val errorMessage: String? = null, - // Wall-clock at which the current Scanning / AutoImporting phase - // started. Drives the "Skip" affordance that the UI reveals after - // [SKIP_REVEAL_DELAY_MS] when the scan is taking too long. Null - // outside scan phases. + val scanStartedAtMs: Long? = null, val isSkipAvailable: Boolean = false, ) { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt index 2989a1d36..5b2409d57 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportViewModel.kt @@ -57,19 +57,11 @@ class ExternalImportViewModel( private val tweaksRepository: zed.rainxch.core.domain.repository.TweaksRepository, ) : ViewModel() { private var candidatesByPackage: Map = emptyMap() - // Cached so OnAutoSummaryUndoAll can re-build cards for previously - // auto-linked packages without round-tripping resolveMatches() (which - // would issue another network call and could return different matches). + private var lastResolvedMatches: List = emptyList() - // Mirror of autoLinkedPackages: per-package pre-link snapshot of whether an - // installed_apps row already existed. Bulk undo consults this to avoid - // wiping rows that pre-existed (e.g., the user had previously linked the - // app through some other path before auto-link added an entry to it). + private var autoLinkedHadInstalledRow: Map = emptyMap() - // Per-package external_links snapshot captured BEFORE auto-link writes the - // MATCHED row. Bulk undo restores from these so the DAO actually rolls back - // to the pre-link state (typically PENDING_REVIEW). Snapshotting AFTER the - // link would just re-apply the linked state — a silent no-op. + private var autoLinkedPreSnapshots: Map = emptyMap() private var hasStarted = false private var scanJob: Job? = null @@ -131,10 +123,10 @@ class ExternalImportViewModel( is ExternalImportAction.OnToggleCardExpanded -> toggleCardExpanded(action.packageName) is ExternalImportAction.OnSearchOverrideChanged -> { - // Explicit submit only — we never auto-fire on keystrokes. + _state.update { if (it.activeSearchPackage != action.packageName) { - // Switched cards: drop stale results from the previous card. + it.copy( activeSearchPackage = action.packageName, searchQuery = action.query, @@ -182,8 +174,7 @@ class ExternalImportViewModel( } else { current.expandedPackages.toPersistentSet().add(packageName) } - // Clear cross-card search results when collapsing the active card so - // they don't bleed into the next card the user expands. + val keepSearch = current.activeSearchPackage == packageName && packageName in nextSet current.copy( expandedPackages = nextSet, @@ -201,9 +192,7 @@ class ExternalImportViewModel( private fun startScanIfIdle(force: Boolean = false) { if (!force && _state.value.phase != ImportPhase.Idle) return if (scanJob?.isActive == true) return - // Skip affordance: stays hidden while the scan is fast, fades in - // after SKIP_REVEAL_DELAY_MS so the user can bail out of a slow - // resolveMatches without backgrounding the app. + skipRevealJob?.cancel() skipRevealJob = viewModelScope.launch { kotlinx.coroutines.delay(SKIP_REVEAL_DELAY_MS) @@ -249,15 +238,11 @@ class ExternalImportViewModel( buildCard(candidate, match) }.toImmutableList() - // Cancel the skip-reveal timer — we've exited the - // long-running phase, so the Skip button shouldn't - // ambiently appear over the next screen. skipRevealJob?.cancel() skipRevealJob = null if (autoLinked.isNotEmpty()) { - // Stop on the summary screen so the user sees what auto-linked - // and can undo before we cascade into the review wizard. + val autoLinkedLabels = autoLinked.mapNotNull { pkg -> candidatesByPackage[pkg]?.appLabel } @@ -319,13 +304,6 @@ class ExternalImportViewModel( } } - /** - * Bail out of an in-flight scan / auto-import. Surfaces whatever - * candidates the resolver got far enough to expose, so the user can - * still review them manually instead of being trapped on a spinner. - * If nothing has been resolved yet, drops to an empty Done state — - * effectively "no candidates this scan, move on". - */ private fun skipLongScan() { val active = scanJob ?: return if (!active.isActive) return @@ -377,11 +355,7 @@ class ExternalImportViewModel( private fun submitSearchOverride(packageName: String) { val current = _state.value - // Search submit applies to whichever card was last typed in. If the - // active package and the submitted package mismatch, we still honour - // the submit using the active query — this only happens if the user - // taps the icon in a different card before the keystrokes registered, - // which the UI prevents via per-card query binding. + if (current.activeSearchPackage != packageName) return val query = current.searchQuery.trim() @@ -397,11 +371,6 @@ class ExternalImportViewModel( return } - // Fast-path: a host/owner/repo URL bypasses the search API and - // surfaces a single MANUAL suggestion that the user can tap to - // link. Accepts GitHub, Codeberg, known Forgejo / Gitea hosts, - // and anything the user has added under Tweaks → Network → - // Custom forges. Matches Obtainium's "paste a URL" mental model. searchJob?.cancel() _state.update { it.copy(isSearching = true, searchError = null) } searchJob = viewModelScope.launch { @@ -436,7 +405,6 @@ class ExternalImportViewModel( return@launch } - // Not a URL — backend free-text search. val result = runCatching { externalImportRepository.searchRepos(query) } .getOrElse { e -> if (e is CancellationException) throw e @@ -476,9 +444,7 @@ class ExternalImportViewModel( private fun skipPackage(packageName: String, neverAsk: Boolean) { val card = _state.value.cards.firstOrNull { it.packageName == packageName } ?: return viewModelScope.launch { - // Distinguish "no prior row" (success → null) from "couldn't read" - // (failure). Treating the latter as null would let undoLast's - // fallback unlink wipe a row that should have been preserved. + val snapshotResult = runCatching { externalImportRepository.snapshotDecision(packageName) } @@ -494,9 +460,6 @@ class ExternalImportViewModel( val snapshot = snapshotResult.getOrNull() val hadInstalledRow = installedAppsRepository.getAppByPackage(packageName) != null - // Short-circuit on failure: don't remove the card, don't offer undo, - // don't fire telemetry — the DAO state is unchanged. Surface an error - // so the user knows the action didn't take effect. val ok = try { externalImportRepository.skipPackage(packageName, neverAsk = neverAsk) true @@ -600,9 +563,7 @@ class ExternalImportViewModel( if (preselect != null) { pickSuggestion(packageName, preselect) } else { - // No preselection means there's nothing to confidently link to. The - // list-mode UI hides the link CTA in this case, but defensively - // surface the expand affordance instead of silently dropping. + toggleCardExpanded(packageName) } } @@ -612,9 +573,7 @@ class ExternalImportViewModel( viewModelScope.launch { try { - // Run rollback DAO ops without swallowing — any failure must - // abort and preserve `pendingUndo` so the user can retry from - // the snackbar. UI state is mutated only after every op succeeds. + if (undo.kind == PendingUndo.Kind.Link && !undo.hadInstalledAppRowBefore) { installedAppsRepository.deleteInstalledApp(undo.packageName) } @@ -625,7 +584,6 @@ class ExternalImportViewModel( externalImportRepository.unlink(undo.packageName) } - // All DAO ops succeeded — now mutate UI and consume the token. pendingUndo = null _state.update { current -> if (current.cards.any { it.packageName == undo.packageName }) { @@ -652,7 +610,7 @@ class ExternalImportViewModel( throw e } catch (e: Exception) { logger.error("Undo failed for ${undo.packageName}: ${e.message}") - // Preserve pendingUndo so the snackbar can offer Undo again. + _events.send( ExternalImportEvent.ShowError( getString(Res.string.external_import_undo_failed), @@ -665,8 +623,7 @@ class ExternalImportViewModel( private fun autoSummaryContinue() { val current = _state.value if (current.phase != ImportPhase.AutoImportSummary) return - // User accepted the auto-imports; the pre-link metadata is no - // longer needed and shouldn't leak into a subsequent wizard run. + autoLinkedHadInstalledRow = emptyMap() autoLinkedPreSnapshots = emptyMap() if (current.cards.isNotEmpty()) { @@ -691,20 +648,11 @@ class ExternalImportViewModel( return } - // Snapshot the pre-link maps locally so a concurrent reset can't race us. val hadInstalledMap = autoLinkedHadInstalledRow val preSnapshots = autoLinkedPreSnapshots viewModelScope.launch { - // Roll each auto-linked package back to its PRE-LINK external_links - // state using the snapshot captured BEFORE materializeAndMark wrote - // the MATCHED row. installed_apps is only deleted for packages whose - // row did NOT pre-exist before auto-link — same policy as undoLast. - // - // Fail-fast: any DAO failure aborts the bulk undo before we touch - // UI state or clear the pre-link maps. The user keeps seeing the - // AutoImportSummary screen and can retry. Already-rolled-back - // packages stay rolled back (idempotent on retry). + try { packages.forEach { pkg -> val preSnapshot = preSnapshots[pkg] @@ -715,7 +663,7 @@ class ExternalImportViewModel( if (preSnapshot != null) { externalImportRepository.restoreDecision(preSnapshot) } else { - // No pre-link row existed — drop the auto-link row entirely. + externalImportRepository.unlink(pkg) } } @@ -731,9 +679,6 @@ class ExternalImportViewModel( return@launch } - // All rollbacks succeeded — invalidate the single-row undo token, - // clear the per-package metadata so a subsequent wizard run can't - // see stale pre-link snapshots, and rebuild the wizard. pendingUndo = null autoLinkedHadInstalledRow = emptyMap() autoLinkedPreSnapshots = emptyMap() @@ -764,8 +709,7 @@ class ExternalImportViewModel( } private fun emitPermissionOutcome(granted: Boolean, sdkInt: Int?) { - // Telemetry removed — kept signature so existing call sites compile; - // safe to drop entirely once those are migrated. + } private fun bucketSdkInt(sdkInt: Int?): String = @@ -793,9 +737,7 @@ class ExternalImportViewModel( if (remaining.isEmpty()) return viewModelScope.launch { - // Track per-package outcome so a partial failure doesn't claim the - // entire wizard cleared and doesn't fire confetti / Done telemetry - // on packages whose DAO state is unchanged. + val successes = mutableSetOf() val failures = mutableListOf() remaining.forEach { card -> @@ -810,9 +752,6 @@ class ExternalImportViewModel( } } - // Skip-remaining is intentionally not undoable — bulk skip clears - // the wizard and triggers the completion screen, and a single - // snackbar isn't a sensible affordance for "undo seven things". pendingUndo = null val allSucceeded = failures.isEmpty() @@ -853,16 +792,6 @@ class ExternalImportViewModel( if (top.confidence < AUTO_LINK_THRESHOLD) return@forEach val candidate = candidatesByPackage[result.packageName] ?: return@forEach - // Capture pre-link state BEFORE materializeAndMark writes the - // MATCHED row. Bulk undo uses both: the pre-link snapshot to - // restore the DAO row to its original state, and the - // installed_apps presence flag to decide whether to delete the - // installed_apps row (only if auto-link created it). - // - // Snapshot failure must skip the auto-link entirely — without a - // reliable pre-link snapshot, undo would fall back to unlink and - // wipe a row that should have been preserved. Push the candidate - // through manual review instead. val snapshotResult = runCatching { externalImportRepository.snapshotDecision(result.packageName) } @@ -946,8 +875,7 @@ class ExternalImportViewModel( "external_links upsert failed for ${candidate.packageName}: " + "${linkResult.exceptionOrNull()?.message}", ) - // installed_apps row is already written; the audit trail is - // ahead but recoverable on the next scan via mergeCandidate. + } return true } @@ -965,9 +893,7 @@ class ExternalImportViewModel( packageName: String, tally: (ExternalImportState) -> ExternalImportState, ) { - // _state.update may invoke the lambda multiple times under contention; - // never assign captured vars from inside it. Read the post-update - // state to decide whether to fire the completion event. + _state.update { current -> val newCards = current.cards.filterNot { it.packageName == packageName }.toImmutableList() val tallied = tally(current).copy( @@ -1004,10 +930,7 @@ class ExternalImportViewModel( RepoMatchSource.SEARCH -> SuggestionSource.SEARCH RepoMatchSource.FINGERPRINT -> SuggestionSource.FINGERPRINT RepoMatchSource.MANUAL -> SuggestionSource.MANUAL - // Forgejo hits map to the same UI bucket as GitHub - // search hits — both are free-text matches against - // a remote registry; the differentiator is the - // sourceHost field on the suggestion itself. + RepoMatchSource.FORGEJO_SEARCH -> SuggestionSource.SEARCH }, stars = stars, @@ -1016,10 +939,7 @@ class ExternalImportViewModel( ) private suspend fun InstallerKind.toUiLabel(): String = - // Exhaustive: STORE_PLAY / STORE_AURORA / STORE_GALAXY / STORE_OEM_OTHER / - // SYSTEM are filtered out at the scanner today, but the wizard's enum - // contract is shared with the scanner — handle every value explicitly so - // a future scanner change can't silently mislabel a candidate. + when (this) { InstallerKind.STORE_OBTAINIUM -> getString(Res.string.external_import_installer_obtainium) InstallerKind.STORE_FDROID -> getString(Res.string.external_import_installer_fdroid) @@ -1052,10 +972,6 @@ class ExternalImportViewModel( private const val PRESELECT_MIN = 0.5 private const val PRESELECT_MAX = 0.85 - // Skip affordance reveal delay. Below this, scans complete - // quickly enough that an escape hatch would just be visual - // noise. Past it, the user assumes something is stuck and - // wants a way out. private const val SKIP_REVEAL_DELAY_MS = 5_000L } } @@ -1063,9 +979,7 @@ class ExternalImportViewModel( private fun parseGithubRepoUrl(input: String): Pair? { val trimmed = input.trim().removeSuffix("/") if (trimmed.isEmpty()) return null - // Accept both bare host references and full https URLs. Anything else - // (search keywords, partial slugs without owner) falls through to the - // backend search path. + val withoutScheme = trimmed .removePrefix("https://") .removePrefix("http://") diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt index b7db28be7..5af5e478a 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt @@ -91,10 +91,6 @@ fun CandidateCard( PreselectedRow(suggestion = candidate.preselectedSuggestion) - // Collapsed footer: primary Link CTA (or hint) + expand affordance. - // The whole card is clickable to expand, but a dedicated control - // gives the disclosure a clear screen-reader role and a tap target - // that doesn't fight the underlying CTA buttons in expanded mode. if (!expanded) { CollapsedActions( canLink = candidate.preselectedSuggestion != null, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ConfettiOverlay.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ConfettiOverlay.kt index 952eaeb6d..b333ef66c 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ConfettiOverlay.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ConfettiOverlay.kt @@ -50,8 +50,7 @@ fun ConfettiOverlay( val particles = remember(palette) { val rng = Random(42) List(40) { index -> - // Use error sparingly — every 7th particle, and primary/secondary/tertiary - // for the rest with primaryContainer mixed in for variety. + val color = when { index % 7 == 6 -> palette[4] index % 5 == 0 -> palette[3] @@ -95,7 +94,7 @@ fun ConfettiOverlay( particles.forEach { p -> val x = p.xFraction * width val y = -40f + travel * p.fallSpeed * progress.value - // Stop drawing once a particle has cleared the bottom. + if (y > height + p.radiusPx) return@forEach val rotation = p.rotationOffset + p.rotationSpeed * progress.value diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt index cacfb5a1a..4f3379fde 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt @@ -82,10 +82,6 @@ fun ImportProgressScreen( textAlign = TextAlign.Center, ) - // Skip affordance fades in once the scan crosses the - // long-running threshold (the VM flips `canSkip` after - // SKIP_REVEAL_DELAY_MS). Hidden during a fast scan so it - // doesn't add a flicker the user has no time to notice. AnimatedVisibility( visible = canSkip, enter = fadeIn(), diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt index 05006b63e..cbefc8220 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt @@ -80,12 +80,7 @@ fun PermissionRationaleScreen( Button(onClick = { scope.launch { onAction(ExternalImportAction.OnRequestPermission) - // QUERY_ALL_PACKAGES is install-time only on stock Android. - // Either we already have visibility (manifest grant honoured) - // → proceed Granted; or we don't → proceed Denied and let the - // scanner's heuristic-degraded path handle it. We never - // dispatch Granted optimistically — that would lie to - // telemetry and skip the degraded-path UX in EmptyStateScreen. + val action = if (requester.isGranted()) { ExternalImportAction.OnPermissionGranted(sdkInt) } else { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoCandidateRow.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoCandidateRow.kt index 85a68775a..3eef2f66d 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoCandidateRow.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoCandidateRow.kt @@ -86,10 +86,7 @@ fun RepoCandidateRow( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) - // Source chip: where this suggestion actually lives. - // Distinct tonal color so a Codeberg row visibly stands - // apart from a GitHub one — answers "why is this here - // and where is it from". + SuggestionHostChip(suggestion.sourceHost) } if (!suggestion.description.isNullOrBlank()) { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/model/RepoSuggestionUi.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/model/RepoSuggestionUi.kt index 94173904e..52357a540 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/model/RepoSuggestionUi.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/model/RepoSuggestionUi.kt @@ -7,9 +7,7 @@ data class RepoSuggestionUi( val source: SuggestionSource, val stars: Int? = null, val description: String? = null, - // Non-null when the suggestion lives on a non-GitHub forge. - // Carries through from import paste / smart-match to the linker - // so the row is actually stored against the right host. + val sourceHost: String? = null, ) { val ownerSlashRepo: String get() = "$owner/$repo" diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt index e094885b1..764ea38b3 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerViewModel.kt @@ -88,7 +88,7 @@ class StarredPickerViewModel( } runCatching { starredRepository.syncStarredRepos(forceRefresh = false) } - .onFailure { /* fall through to local cache */ } + .onFailure { } val starred = starredRepository.getAllStarred().first() val tracked = installedAppsRepository.getAllInstalledApps().first() diff --git a/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.jvm.kt b/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.jvm.kt index 80f2be7e1..065a5a0fc 100644 --- a/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.jvm.kt +++ b/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/import/util/ReducedMotionProvider.jvm.kt @@ -2,8 +2,5 @@ package zed.rainxch.apps.presentation.import.util import androidx.compose.runtime.Composable -// Desktop has no equivalent system "remove animations" toggle that the -// JVM Compose target can read portably. The wizard isn't shown on -// Desktop today (E2 territory), so this is a safe default. @Composable actual fun rememberSystemReducedMotion(): Boolean = false diff --git a/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/network/GitHubAuthApi.kt b/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/network/GitHubAuthApi.kt index 53cc9d289..d7b9194fd 100644 --- a/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/network/GitHubAuthApi.kt +++ b/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/network/GitHubAuthApi.kt @@ -62,14 +62,6 @@ object GitHubAuthApi { } } - /** - * Dedicated client for PAT validation — NO retries. Validation is a - * user-blocking synchronous step (sheet is spinning while we call - * this), so a 30s retry cascade defeats the UX for anyone on a - * degraded network. One shot, 10s timeout, done. Falls back to the - * "Unreachable" path on any failure, which the caller handles by - * saving optimistically. - */ private val validationHttp by lazy { HttpClient { install(ContentNegotiation) { json(json) } @@ -284,21 +276,6 @@ object GitHubAuthApi { } } - /** - * Validates a Personal Access Token by calling GitHub's `/user` - * endpoint with it. Three outcomes: - * - * [PatValidation.Valid] → 2xx response, token authenticates cleanly. - * [PatValidation.Rejected] → 401 or 403, token is bad or revoked. - * [PatValidation.Unreachable] → network/timeout, we couldn't ask. - * - * The `Unreachable` case is deliberately distinct from `Rejected`: - * many users paste a PAT precisely because their network can't - * reach GitHub reliably (China, corporate firewalls). Treating - * unreachable as a rejection would block the whole feature for - * exactly the people who need it most. Caller decides whether to - * save optimistically on `Unreachable`. - */ suspend fun validatePersonalAccessToken(token: String): PatValidation { return try { val res = validationHttp.get("https://api.github.com/user") { @@ -313,9 +290,7 @@ object GitHubAuthApi { PatValidation.Rejected(RejectedKind.BadCredentials) status == HttpStatusCode.Forbidden -> PatValidation.Rejected(RejectedKind.InsufficientScope) - // Non-auth 4xx (400, 422, etc.): treat as a definitive reject - // rather than "unreachable" — GitHub could answer, it just - // said no. Retrying won't help. + status.value in 400..499 -> PatValidation.Rejected(RejectedKind.Other(status.value)) else -> PatValidation.Unreachable("HTTP ${status.value}") diff --git a/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/repository/AuthenticationRepositoryImpl.kt b/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/repository/AuthenticationRepositoryImpl.kt index cf1f82ac6..46022ec12 100644 --- a/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/repository/AuthenticationRepositoryImpl.kt +++ b/feature/auth/data/src/commonMain/kotlin/zed/rainxch/auth/data/repository/AuthenticationRepositoryImpl.kt @@ -388,25 +388,13 @@ class AuthenticationRepositoryImpl( override suspend fun signInWithPat(token: String): Result = withContext(Dispatchers.IO) { val trimmed = token.trim() - // Format gatekeeping first: trip the obvious paste-errors - // (trailing whitespace, partial copy) before even making a - // network call. + if (!looksLikePat(trimmed)) { return@withContext Result.failure( IllegalArgumentException("Token format not recognized"), ) } - // Network validation via GitHub's /user endpoint. Three - // outcomes: Valid → proceed, Rejected → fail immediately - // (don't persist a known-bad token), Unreachable → proceed - // optimistically. The Unreachable case is important: the - // whole reason users use this flow is that their network - // can't reliably reach github.com. Blocking the save on - // unreachability would defeat the feature for China users. - // A bad-but-couldn't-validate token will still surface a - // 401 on the first authenticated API call, and the existing - // 401 handler will clear it cleanly. when (val validation = GitHubAuthApi.validatePersonalAccessToken(trimmed)) { is PatValidation.Valid -> { logger.debug("PAT network-validated against GitHub /user") @@ -446,17 +434,6 @@ class AuthenticationRepositoryImpl( } } - /** - * Accepts the two PAT shapes users can create from GitHub's UI: - * - classic: `ghp_` + ~36 chars - * - fine-grained: `github_pat_` + ~82 chars - * - * Deliberately rejects GitHub App / OAuth tokens (`ghs_`, `gho_`, - * `ghu_`, `ghr_`) — they can authenticate but have different - * expiry/refresh semantics than PATs and would need separate - * handling to be safe here. Length check is lenient on purpose so - * a future GitHub format bump doesn't silently lock us out. - */ private fun looksLikePat(token: String): Boolean { if (token.length < 20) return false if (token.any { it.isWhitespace() }) return false diff --git a/feature/auth/domain/src/commonMain/kotlin/zed/rainxch/auth/domain/repository/AuthenticationRepository.kt b/feature/auth/domain/src/commonMain/kotlin/zed/rainxch/auth/domain/repository/AuthenticationRepository.kt index 97a0c77a1..e16b00423 100644 --- a/feature/auth/domain/src/commonMain/kotlin/zed/rainxch/auth/domain/repository/AuthenticationRepository.kt +++ b/feature/auth/domain/src/commonMain/kotlin/zed/rainxch/auth/domain/repository/AuthenticationRepository.kt @@ -16,30 +16,6 @@ interface AuthenticationRepository { path: AuthPath, ): PollOutcome - /** - * Saves a user-supplied Personal Access Token as the active auth - * credential. - * - * Validation flow: - * 1. Client-side format check (rejects obvious paste-errors). - * 2. Network-side check against GitHub's `/user` endpoint — if - * GitHub returns 401/403 we reject and do NOT persist. If GitHub - * is unreachable (timeout/DNS/block), we persist optimistically. - * A bad-but-unreachable token will surface a 401 on the first - * real authenticated call, same as any expired token. - * - * Use case: users on networks where the browser-side of device flow - * (reaching `github.com/login/device`) is unreliable — they generate - * a PAT on a device where GitHub works, paste it here, and skip the - * browser dance entirely. Unreachable-but-save-anyway is deliberate: - * the whole reason this feature exists is for users who can't reach - * GitHub reliably in the moment. - * - * @return [Result.success] on persist, [Result.failure] on client-side - * format error or GitHub-side 401/403 rejection. On [Result.failure] - * the caller should keep the input sheet open so the user can fix - * the token. - */ suspend fun signInWithPat(token: String): Result suspend fun registerWebAuth(): Result @@ -74,25 +50,13 @@ sealed interface DevicePollResult { data class Failed(val error: Throwable) : DevicePollResult } -/** - * Reason a Personal Access Token was rejected by GitHub. Lives in the - * domain layer so the VM can pattern-match and map to localized strings - * without any raw English leaking through from the data layer. - */ sealed interface RejectedKind { - /** GitHub returned 401 — token is invalid or has been revoked. */ + data object BadCredentials : RejectedKind - /** GitHub returned 403 — token lacks required permissions/scopes or is banned. */ data object InsufficientScope : RejectedKind - /** Any other non-2xx status that still represents a definitive reject. */ data class Other(val statusCode: Int) : RejectedKind } -/** - * Thrown by `signInWithPat` when GitHub definitively rejects the token - * (as opposed to "we couldn't reach GitHub to ask"). Carries a typed - * [kind] so the presentation layer can display a localized error. - */ class PatRejectedException(val kind: RejectedKind) : Exception("PAT rejected: $kind") diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationAction.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationAction.kt index 2ad5b4814..d8f150a74 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationAction.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationAction.kt @@ -27,7 +27,6 @@ sealed interface AuthenticationAction { data object OnResumed : AuthenticationAction - // PAT paste flow data object OpenPatSheet : AuthenticationAction data object DismissPatSheet : AuthenticationAction @@ -40,7 +39,6 @@ sealed interface AuthenticationAction { data object OpenPatSettingsPage : AuthenticationAction - // Web OAuth flow (default in 1.8.3). Device flow + PAT remain as fallbacks. data object StartWebAuth : AuthenticationAction data class ConsumeAuthHandoff( diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 528b497f9..171bf0856 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -750,11 +750,7 @@ private fun PatSignInSheet( ) { val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, - // Veto the visual Hidden transition while a submission is in - // flight, so a swipe-down/scrim-tap can't cosmetically dismiss - // the sheet before `onDismissRequest`'s guard runs. Pairs with - // the existing `!isSubmitting` check in `onDismissRequest` to - // fully gate dismissal during save. + confirmValueChange = { newValue -> !(isSubmitting && newValue == SheetValue.Hidden) }, diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationState.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationState.kt index 18e8e0224..d624e1042 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationState.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationState.kt @@ -8,15 +8,12 @@ data class AuthenticationState( val info: String? = null, val isPolling: Boolean = false, val pollIntervalSec: Int = 0, - // PAT ("Personal Access Token") paste flow — alternate login path - // for users on networks where the browser can't reach github.com - // to complete the device-flow authorize step. + val isPatSheetVisible: Boolean = false, val patInput: String = "", val patError: String? = null, val isPatSubmitting: Boolean = false, - // Web OAuth flow is the default in 1.8.3. Device flow + PAT live behind - // this expandable for users who need them (corp firewalls, GFW etc). + val isAdvancedAuthVisible: Boolean = false, val isWebAuthInFlight: Boolean = false, ) diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationViewModel.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationViewModel.kt index adbaf062f..e65047d4b 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationViewModel.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationViewModel.kt @@ -48,14 +48,6 @@ class AuthenticationViewModel( private var pollingIntervalMs: Long = DEFAULT_POLL_INTERVAL_SEC * 1000L private var authPath: AuthPath = AuthPath.Backend - /** - * Wall-clock timestamp (`System.currentTimeMillis()`) when the most - * recent poll *started*. Used to dedupe user-triggered polls against - * the background polling loop so that rapid interactions (tapping - * "Check status" multiple times, reopening the app repeatedly) don't - * stack polls on top of each other and trigger GitHub `slow_down` - * responses — the root cause of the "Rate limited" cascade. - */ private var lastPollStartedAtMs: Long = 0L private val _state: MutableStateFlow = @@ -147,9 +139,7 @@ class AuthenticationViewModel( } AuthenticationAction.DismissPatSheet -> { - // Cancel any in-flight submission so it can't race past - // the dismissal and navigate/toast after the user has - // bailed on the sheet. + patSubmissionJob?.cancel() patSubmissionJob = null _state.update { @@ -164,8 +154,7 @@ class AuthenticationViewModel( is AuthenticationAction.OnPatInputChanged -> { _state.update { - // Clear error on edit so it doesn't linger after the - // user starts fixing the problem. + it.copy(patInput = action.input, patError = null) } } @@ -329,9 +318,7 @@ class AuthenticationViewModel( private fun consumeAuthHandoff(handoffId: String, state: String) { val expected = savedStateHandle.get(KEY_WEB_AUTH_STATE) - // Custom scheme is public — any app can fire githubstore://auth?... - // No pending session = not our flow. Drop silently to avoid knocking - // an unsuspecting user into an error screen. + if (expected == null) { logger.debug("Ignoring web-auth handoff with no pending session") return @@ -472,16 +459,8 @@ class AuthenticationViewModel( if (loginState !is AuthLoginState.DevicePrompt) return if (_state.value.isPolling) return - // If the background loop is alive it's already polling on a fixed - // schedule — ANY on-resume poll we fire here lands on top of it and - // triggers `slow_down`. (Previous implementation tried to time-window - // this; the window was too narrow and still raced.) With a healthy - // loop we trust it completely and do nothing on resume. if (pollingJob?.isActive == true) return - // Loop died between sessions (process death without restoreFromSavedState - // rehydrating, or a crash in the loop itself). Restart + immediate poll - // to get things moving again. logger.debug("Resume poll: background loop was dead, restarting") startPolling(loginState.start.deviceCode) pollOnce(loginState.start.deviceCode) @@ -492,9 +471,6 @@ class AuthenticationViewModel( if (loginState !is AuthLoginState.DevicePrompt) return val deviceCode = loginState.start.deviceCode - // Hard-block manual polls that land within MIN_MANUAL_POLL_SPACING_MS - // of the last poll. Prevents user tap-spam from burning the - // `slow_down` budget. val sinceLast = System.currentTimeMillis() - lastPollStartedAtMs if (sinceLast < MIN_MANUAL_POLL_SPACING_MS) { logger.debug("Manual poll suppressed — only ${sinceLast}ms since last poll") @@ -502,9 +478,7 @@ class AuthenticationViewModel( } logger.debug("Manual poll requested (pollingJobActive=${pollingJob?.isActive})") - // Restart the background loop so its next scheduled poll is a full - // interval AFTER this manual one, not stacked right on top. Preserve - // the adaptive interval — don't reset it on manual tap. + startPolling(deviceCode, resetInterval = false) pollOnce(deviceCode) } @@ -585,7 +559,7 @@ class AuthenticationViewModel( val intervalSec = (loginState as? AuthLoginState.DevicePrompt)?.start?.intervalSec ?: DEFAULT_POLL_INTERVAL_SEC - // Add 1s buffer above GitHub's minimum to avoid immediate slow_down + pollingIntervalMs = (intervalSec * 1000).toLong() + 1000L } pollingJob = @@ -598,10 +572,7 @@ class AuthenticationViewModel( } private fun pollOnce(deviceCode: String) { - // Set the timestamp SYNCHRONOUSLY here (not inside doPoll's suspend - // body) so any concurrent tryPollIfReady / forcePollNow / restore - // invocation sees the current poll reservation immediately and - // doesn't stack another poll on top. + lastPollStartedAtMs = System.currentTimeMillis() viewModelScope.launch { doPoll(deviceCode) @@ -619,17 +590,7 @@ class AuthenticationViewModel( } if (outcome.path != authPath) { - // Mid-session transitions are strictly one-way: only the - // Backend → Direct escalation that AuthenticationRepositoryImpl - // performs on infrastructure errors (timeouts, connect/socket - // failures, 5xx) is legitimate. Direct → Backend is not a path - // the repository ever returns; treat any such outcome as a - // defensive no-op rather than silently flipping back to a - // probably-broken Backend mid-flow. The infrastructure-failure - // gate itself lives at the repository (single source of - // truth — see `pollDeviceTokenOnce` and - // `Throwable.isAuthInfrastructureError()`); the VM only - // enforces direction. + val isLegalEscalation = authPath == AuthPath.Backend && outcome.path == AuthPath.Direct if (isLegalEscalation) { @@ -661,10 +622,7 @@ class AuthenticationViewModel( } is DevicePollResult.SlowDown -> { - // Cap the interval so one rough patch of rapid polls - // (e.g. several ON_RESUME stacks early in the session) - // can't strand the user waiting 30+ seconds to pick - // up a completed authorization. + val bumped = (pollingIntervalMs + 5000L).coerceAtMost(MAX_POLL_INTERVAL_MS) val clamped = bumped == MAX_POLL_INTERVAL_MS && pollingIntervalMs >= MAX_POLL_INTERVAL_MS pollingIntervalMs = bumped @@ -681,8 +639,7 @@ class AuthenticationViewModel( pollIntervalSec = (pollingIntervalMs / 1000).toInt(), ) } - // Don't restart — the existing polling loop reads pollingIntervalMs - // on each iteration via delay(), so it will pick up the new value. + } is DevicePollResult.Failed -> { @@ -708,8 +665,6 @@ class AuthenticationViewModel( } } - // region SavedStateHandle - private fun saveToSavedState( deviceCode: String, startUi: GithubDeviceStartUi, @@ -774,8 +729,6 @@ class AuthenticationViewModel( pollOnce(deviceCode) } - // endregion - private suspend fun categorizeError(t: Throwable): Pair { val msg = t.message ?: return getString(Res.string.error_unknown) to null val lowerMsg = msg.lowercase() @@ -869,19 +822,8 @@ class AuthenticationViewModel( private const val KEY_WEB_AUTH_STATE = "auth_web_state" private const val DEFAULT_POLL_INTERVAL_SEC = 5 - /** - * Minimum wall-clock gap between a user-initiated manual poll - * (tap "Check status") and the previous poll. Anything closer - * gets silently dropped to keep us out of `slow_down` territory. - */ private const val MIN_MANUAL_POLL_SPACING_MS = 2_000L - /** - * Ceiling on the adaptive `pollingIntervalMs`. Without this cap, - * a run of `slow_down` responses could push the interval up by - * 5s each time, leaving the user waiting 30+ seconds for the - * app to notice their completed authorization. - */ private const val MAX_POLL_INTERVAL_MS = 15_000L private const val PAT_SETTINGS_URL = "https://github.com/settings/tokens/new" diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt index 231bfb9e0..2ca2eb126 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt @@ -91,25 +91,6 @@ class DetailsRepositoryImpl( private val readmeHelper = ReadmeLocalizationHelper(localizationManager) - /** - * Decides whether a backend failure should trigger the direct-to-GitHub - * fallback. **Side effect:** rethrows `CancellationException` to preserve - * structured concurrency — callers don't need a separate CE check before - * invoking this. - * - * Returns `true` for: - * - Any non-[BackendException] throwable (network errors, timeouts, - * parse failures — all treated as infra) - * - [BackendException] with status in 500..599 - * - * Returns `false` for: - * - [BackendException] with status in 400..499 — backend's answer is - * authoritative (cached 404, 401 auth failure, 429 rate limit, etc.) - * and GitHub-direct would return the same answer. **Note:** this - * includes 429 and 408 — if the backend is rate-limiting us or - * timing out on its own pipeline, retrying via GitHub direct - * doesn't help and only burns more quota. - */ private fun shouldFallbackToGithubOrRethrow(cause: Throwable): Boolean = sharedShouldFallback(cause) @@ -179,8 +160,6 @@ class DetailsRepositoryImpl( return cached } - // Try backend first. Phase 5.1: backend now lazy-caches unknown - // repos, so success rate is high even for non-curated repos. val backendResult = backendApiClient.getRepo(owner, name) backendResult.fold( onSuccess = { backendRepo -> @@ -191,9 +170,7 @@ class DetailsRepositoryImpl( }, onFailure = { e -> if (!shouldFallbackToGithubOrRethrow(e)) { - // Backend 4xx — GitHub would give the same answer. - // Serve stale if we have it, otherwise propagate the - // error so the VM can show the right state. + cacheManager.getStale(cacheKey)?.let { stale -> logger.debug("Backend 4xx for $owner/$name, serving stale cache") return stale @@ -204,7 +181,6 @@ class DetailsRepositoryImpl( }, ) - // Fallback to GitHub API (only reached on backend 5xx / network error) return try { val result = httpClient @@ -329,8 +305,6 @@ class DetailsRepositoryImpl( } } - // Backend-first. Phase 5.1 routes /v1/releases via the backend cache - // + ETag revalidation, China-reachable via Gcore/api-direct. val backendResult = backendApiClient.getReleases(owner, repo) backendResult.fold( onSuccess = { releases -> @@ -358,7 +332,6 @@ class DetailsRepositoryImpl( }, ) - // Fallback to GitHub API directly (only reached on backend 5xx / network error) return try { val releases = httpClient @@ -386,10 +359,7 @@ class DetailsRepositoryImpl( } catch (e: CancellationException) { throw e } catch (e: SerializationException) { - // Parse failure signals a DTO/API drift — surface loudly so it's - // findable in logs and crash reports. Still prefer returning a - // stale cache rather than throwing, so the UI can keep rendering - // the last known good data while we figure out the new shape. + logger.error("Failed to parse releases for $owner/$repo: ${e.message}", e) cacheManager.getStale>(cacheKey)?.let { stale -> logger.debug("Serving stale cache for releases $owner/$repo after parse failure") @@ -427,10 +397,7 @@ class DetailsRepositoryImpl( sourceHost: String?, ): Triple? { if (sourceHost != null) return getForgejoReadme(owner, repo, defaultBranch, sourceHost) - // v2 — bumped after markdown preprocessor overhaul (alerts, - // emoji, details, image-row). Forces re-fetch so users get a - // properly-processed readme instead of waiting for the stale - // v1 entry to expire. + val cacheKey = "details:readme:v4:$owner/$repo" cacheManager.get(cacheKey)?.let { cached -> @@ -438,10 +405,6 @@ class DetailsRepositoryImpl( return Triple(cached.content, cached.languageCode, cached.path) } - // Backend-first. Phase 5.2: /v1/readme proxies GitHub's contents API, - // which returns base64-encoded markdown — different shape from the - // raw.githubusercontent.com path below, but the post-processing - // pipeline is the same. val backendResult = backendApiClient.getReadme(owner, repo) backendResult.fold( onSuccess = { dto -> @@ -458,7 +421,7 @@ class DetailsRepositoryImpl( ) return processed } - // Decode/processing failed — fall through to the raw-URL path + logger.debug("Backend readme decode failed for $owner/$repo, falling back to raw URL") }, onFailure = { e -> @@ -467,17 +430,13 @@ class DetailsRepositoryImpl( logger.debug("Backend 4xx for readme $owner/$repo, serving stale cache") return Triple(stale.content, stale.languageCode, stale.path) } - // No stale — no readme exists or user can't access. Treat - // as "no readme" rather than propagating as an error; - // matches how fetchReadmeFromApi returned null. + return null } logger.debug("Backend infra error for readme $owner/$repo (${e.message}), falling back to raw URL") }, ) - // Fallback to raw.githubusercontent.com (only reached on backend - // infra error or on successful backend response that we couldn't decode) val result = fetchReadmeFromApi(owner, repo, defaultBranch) if (result != null) { @@ -500,10 +459,7 @@ class DetailsRepositoryImpl( repo: String, defaultBranch: String, ): Triple? { - // GitHub's contents API base64-encodes with embedded newlines; Mime - // variant tolerates all whitespace transparently so we don't have - // to pre-strip. Narrow catch: only IAE is decode-related, other - // throwables (OOM, etc.) propagate. + val rawContent = dto.content ?: return null val decoded = try { Base64.Mime.decode(rawContent).decodeToString() @@ -555,9 +511,7 @@ class DetailsRepositoryImpl( sourceHost: String?, ): RepoStats { if (sourceHost != null) return getForgejoRepoStats(owner, repo, sourceHost) - // v3 — backend now supplies license. Bumping the key forces re-fetch - // so post-upgrade users get a populated license instead of waiting - // 6h for the stale v2 entry (license=null) to expire. + val cacheKey = "details:stats:v3:$owner/$repo" cacheManager.get(cacheKey)?.let { cached -> @@ -565,9 +519,6 @@ class DetailsRepositoryImpl( return cached } - // Try backend first — provides stars/forks/openIssues/license/downloadCount. - // No more direct GitHub enrichment for license (was 1 quota hit per - // signed-in user per stats fetch); backend is now authoritative. val backendResult = backendApiClient.getRepo(owner, repo) backendResult.fold( onSuccess = { backendRepo -> @@ -594,7 +545,6 @@ class DetailsRepositoryImpl( }, ) - // Fallback to GitHub API return try { logger.debug("Backend miss for stats $owner/$repo, falling back to GitHub API") val info = @@ -633,8 +583,6 @@ class DetailsRepositoryImpl( return cached } - // Backend-first. Phase 5.3: /v1/user proxies GitHub's users API with - // aggressive edge caching (7-day TTL on Gcore). val backendResult = backendApiClient.getUser(username) backendResult.fold( onSuccess = { user -> @@ -654,7 +602,6 @@ class DetailsRepositoryImpl( }, ) - // Fallback to GitHub direct (only reached on backend 5xx / network error) return try { val user = httpClient @@ -693,14 +640,6 @@ class DetailsRepositoryImpl( twitterUsername = twitterUsername, ) - // ── Forgejo / Codeberg branch ───────────────────────────────────── - // - // No backend proxy, no GitHub fallback — all reads go straight to the - // forge instance. Cache keys are namespaced by host so the same - // `owner/repo` slug on github.com and codeberg.org never collide. Stats - // are derived from the repo response (stars / forks / openIssues / - // license fields exposed by Gitea/Forgejo API v1). - private suspend fun getForgejoRepository( owner: String, name: String, @@ -725,10 +664,7 @@ class DetailsRepositoryImpl( repo: String, sourceHost: String, ): List { - // v2 — release `body` is now pre-processed (CRLF → LF + relative - // URL rewrite to the Forgejo raw endpoint). Previously the raw - // CRLF bodies broke GFM table parsing in the markdown renderer - // (Gadgetbridge changelog tables rendered as literal pipes). + val cacheKey = "details:releases:forgejo:v2:$sourceHost:$owner/$repo" cacheManager.get>(cacheKey)?.takeIf { it.isNotEmpty() }?.let { return it } val client = forgejoClientRegistry.clientFor(sourceHost) @@ -757,11 +693,7 @@ class DetailsRepositoryImpl( owner: String, repo: String, ): String { - // Forgejo emits CRLF line endings. The intellij-markdown GFM - // table parser is line-sensitive — `\r` left in the separator - // row makes the table degrade to literal pipes. Normalize first, - // then run the same image / URL rewriting used on GitHub - // release bodies, but pointed at the Forgejo raw-branch URL. + val normalized = body.replace("\r\n", "\n") val baseUrl = "https://$sourceHost/$owner/$repo/raw/branch/HEAD/" return preprocessMarkdown(markdown = normalized, baseUrl = baseUrl) @@ -774,20 +706,13 @@ class DetailsRepositoryImpl( defaultBranch: String, sourceHost: String, ): Triple? { - // v2 — moved off the non-existent `/readme` endpoint (404s on every - // Forgejo / Gitea instance) onto `/contents/README.md`. + val cacheKey = "details:readme:forgejo:v2:$sourceHost:$owner/$repo" cacheManager.get(cacheKey)?.let { return Triple(it.content, it.languageCode, it.path) } val client = forgejoClientRegistry.clientFor(sourceHost) - // Forgejo / Gitea does NOT implement GitHub's `/repos/{o}/{r}/readme` - // convenience endpoint — verified live against codeberg.org. We hit - // the contents endpoint directly. Try `README.md` first (covers the - // overwhelming majority including Gadgetbridge), and on 404 fall - // back to listing the repo root and scanning for any - // `^README(\..+)?$` file. val dto = client.getContentsFile(owner, repo, "README.md", defaultBranch).getOrNull() ?: client.listContentsRoot(owner, repo, defaultBranch).getOrNull() ?.firstOrNull { entry -> @@ -803,8 +728,6 @@ class DetailsRepositoryImpl( return null } - // Forgejo's contents response is base64 (single continuous line, - // no MIME wrapping — Mime variant tolerates both forms transparently). val rawContent = dto.content ?: return null val decoded = try { Base64.Mime.decode(rawContent).decodeToString() @@ -813,8 +736,7 @@ class DetailsRepositoryImpl( return null } val path = dto.path?.takeIf { it.isNotBlank() } ?: "README.md" - // Relative image refs in a Forgejo README need the per-host raw URL - // base. Forgejo's raw path shape is `/{o}/{r}/raw/branch/{ref}/`. + val baseUrl = "https://$sourceHost/$owner/$repo/raw/branch/$defaultBranch/" val processed = preprocessMarkdown(markdown = decoded, baseUrl = baseUrl) val detected = readmeHelper.detectReadmeLanguage(processed) @@ -827,7 +749,7 @@ class DetailsRepositoryImpl( } private companion object { - // README, README.md, README.rst, README.adoc, Readme.txt, etc. + private val READMEFileNameRegex = Regex("""^README(\..+)?$""", RegexOption.IGNORE_CASE) } @@ -836,17 +758,13 @@ class DetailsRepositoryImpl( repo: String, sourceHost: String, ): RepoStats { - // v2 — added license sniffing (from /contents/LICENSE) + - // aggregated download count (from release assets), neither of - // which Forgejo exposes on the /repos endpoint itself. + val cacheKey = "details:stats:forgejo:v2:$sourceHost:$owner/$repo" cacheManager.get(cacheKey)?.let { return it } val client = forgejoClientRegistry.clientFor(sourceHost) return try { val info = client.getRepository(owner, repo).getOrThrow() - // Best-effort license + downloads enrichment. Both can fail - // silently — repo stats still render with the core counters - // pulled directly from the /repos payload. + val license = detectForgejoLicense(client, owner, repo, info.defaultBranch ?: "main") val downloads = sumForgejoReleaseDownloads(sourceHost, owner, repo) val result = RepoStats( @@ -864,14 +782,6 @@ class DetailsRepositoryImpl( } } - /** - * Forgejo / Gitea does NOT expose a `license` field on - * `/repos/{o}/{r}`. The dedicated `/license` endpoint is - * GitHub-specific (404 on Forgejo). Best-effort: fetch the LICENSE - * file from the repo root and regex-match the first ~200 chars - * against canonical license headers. Returns SPDX-style id when - * matched, falls back to a short label, returns `null` on miss. - */ @OptIn(ExperimentalEncodingApi::class) private suspend fun detectForgejoLicense( client: zed.rainxch.core.data.network.ForgejoApiClient, @@ -879,8 +789,7 @@ class DetailsRepositoryImpl( repo: String, ref: String, ): String? { - // Try common LICENSE filenames in priority order. Most repos use - // bare `LICENSE`; common alternates covered below. + val candidates = listOf("LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING") val dto = candidates.firstNotNullOfOrNull { name -> client.getContentsFile(owner, repo, name, ref).getOrNull() @@ -914,13 +823,6 @@ class DetailsRepositoryImpl( } } - /** - * Forgejo's `/repos/{o}/{r}` response carries no aggregate download - * count. Each release `asset.download_count` is per-asset, and the - * stable channel for total downloads is the sum across all release - * assets. Reuses the cached releases list when present so we don't - * double-fetch when stats + releases load in parallel from the VM. - */ private suspend fun sumForgejoReleaseDownloads( sourceHost: String, owner: String, diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt index a1ba44ce2..97d60986c 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt @@ -20,12 +20,6 @@ import zed.rainxch.details.domain.repository.TranslationRepository import kotlin.time.Clock import kotlin.time.ExperimentalTime -/** - * Orchestrates translation: picks the user-configured [Translator] - * ([TranslationProvider]), drives chunking + caching in this layer - * (so each concrete translator only has to round-trip a single - * chunk), and stitches results back together. - */ class TranslationRepositoryImpl( private val localizationManager: LocalizationManager, private val clientProvider: TranslationClientProvider, @@ -39,8 +33,6 @@ class TranslationRepositoryImpl( isLenient = true } - // Google's provider has no per-install config — share a single - // instance for the lifetime of the repository. private val googleTranslator: GoogleTranslator = GoogleTranslator(httpClient = { httpClient }, json = json) @@ -62,9 +54,6 @@ class TranslationRepositoryImpl( } } - // Mask out machine-readable spans (code fences, HTML tags, - // markdown URLs, bare URLs) so the translator doesn't mangle - // them. See `protectFromTranslation` kdoc for the full list. val protection = protectFromTranslation(text) val translator = resolveTranslator() @@ -102,32 +91,14 @@ class TranslationRepositoryImpl( return result } - /** - * Strips machine-readable spans (code fences, HTML tags, markdown - * link/image URLs) that translators will otherwise mangle. Each span - * is replaced with an opaque marker the translator passes through - * verbatim, then restored after the joined translation comes back. - * - * Why bigger than just fenced code: real READMEs commonly stack - * `[![alt](badge.svg)](https://store/...)` badge rows and raw - * `` tags. Translators tokenize on `(` and `<`, then insert - * whitespace into URLs (observed on `https://appgallery.cloud. - * huawei.com/...`) or translate alt text into the href slot. Both - * corrupt the rendered markdown. - */ private fun protectFromTranslation(text: String): TranslationProtection { val spans = mutableListOf() var masked = text - // 1. Fenced code blocks first — these are the most disruptive - // spans and matching them before everything else means later - // passes won't accidentally re-mask their inner content. masked = Regex("```[\\s\\S]*?```", RegexOption.MULTILINE).replace(masked) { match -> replaceWithMarker(spans, match.value) } - // 2. HTML tags including their bodies (`...`, ``, - // `...`). Translators rewrite `href` and - // `src` attributes; preserve the whole element. + masked = Regex("<[^/!][a-zA-Z0-9]*[^>]*?/>").replace(masked) { match -> replaceWithMarker(spans, match.value) } @@ -140,24 +111,16 @@ class TranslationRepositoryImpl( masked = Regex("]*>", RegexOption.IGNORE_CASE).replace(masked) { match -> replaceWithMarker(spans, match.value) } - // 3. Markdown link / image URLs: the `](url)` tail. Keep the - // `[label]` part untouched so prose-style link text still - // translates (e.g. `[the docs]` → `[la documentación]`). + masked = Regex("\\]\\(([^)]+)\\)").replace(masked) { match -> val url = match.groupValues[1] "](" + replaceWithMarker(spans, url) + ")" } - // 4. Bare URLs in plain text. Without this, translators - // sometimes split long URLs across whitespace. + masked = Regex("https?://[^\\s<>\")]+").replace(masked) { match -> replaceWithMarker(spans, match.value) } - // 5. GFM alert callout markers: `[!NOTE]`, `[!TIP]`, - // `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`. The renderer - // pattern-matches these literally to pick the alert kind / - // icon / tint; translation rewrites `[!IMPORTANT]` into - // e.g. `[!Vajno]` (ru) and the renderer falls back to a - // plain blockquote, losing the formatting entirely. + masked = Regex( "\\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]", RegexOption.IGNORE_CASE, @@ -171,9 +134,7 @@ class TranslationRepositoryImpl( private fun replaceWithMarker(spans: MutableList, value: String): String { val idx = spans.size spans += value - // Unicode math brackets + ALL_CAPS underscore token — empirically - // preserved verbatim by Google + Youdao translators across the 33 - // supported targets. + return "⟦TR_${idx}_END⟧" } @@ -181,9 +142,7 @@ class TranslationRepositoryImpl( if (spans.isEmpty()) return translated var result = translated spans.forEachIndexed { i, original -> - // Tolerate translator inserting whitespace around the marker. - // Falls back to leaving any unmatched marker in place rather - // than corrupting prose. + val pattern = Regex("⟦\\s*TR_\\s*${i}\\s*_END\\s*⟧") result = pattern.replaceFirst(result, Regex.escapeReplacement(original)) } @@ -197,12 +156,6 @@ class TranslationRepositoryImpl( override fun getDeviceLanguageCode(): String = localizationManager.getPrimaryLanguageCode() - /** - * Resolves the currently-selected translator from preferences. - * Called per request rather than held as a field so provider / - * credential changes take effect on the next translation without - * requiring the repository to be rebuilt. - */ private suspend fun resolveTranslator(): Translator { val provider = tweaksRepository.getTranslationProvider().first() return when (provider) { @@ -315,11 +268,8 @@ class TranslationRepositoryImpl( companion object { private const val MAX_CACHE_SIZE = 50 - private const val CACHE_TTL_MS = 30 * 60 * 1000L // 30 minutes - // Public Disroot-hosted LibreTranslate mirror — anonymous, no - // API key required. Used when the user picks LibreTranslate - // without configuring a self-hosted URL. Override in Tweaks → - // Translation when a mirror you trust more is available. + private const val CACHE_TTL_MS = 30 * 60 * 1000L + private const val LIBRE_TRANSLATE_DEFAULT_URL = "https://translate.disroot.org" } diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt index d01210038..9a403402e 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt @@ -68,11 +68,6 @@ class InstallationManagerImpl( val apkInfo = params.apkInfo val repo = params.repo - // Capture the user's variant pick as a fingerprint so the next - // update resolves to the same APK flavour. Returns null for - // single-asset releases or unparseable filenames — in that case - // the pin fields stay null and the resolver falls back to the - // platform auto-picker, same as before this fix. val fingerprint = AssetVariant.fingerprintFromPickedAsset( pickedAssetName = params.assetName, @@ -82,11 +77,6 @@ class InstallationManagerImpl( val pickedIndex = params.pickedAssetIndex?.takeIf { it >= 0 } val siblingCount = params.siblingAssetCount.takeIf { it > 0 } - // New apps inherit the global "include betas" preference - // so users who track betas across the board don't have to - // flip the per-app toggle for every install. Existing - // rows keep their own value; the global toggle is only - // consulted on creation. val defaultIncludePreReleases = runCatching { tweaksRepository.getIncludePreReleases().first() } .getOrDefault(false) @@ -104,11 +94,7 @@ class InstallationManagerImpl( installedVersion = params.releaseTag, installedAssetName = params.assetName, installedAssetUrl = params.assetUrl, - // Leave upstream `latest*` blank on first-install. The - // user may have picked an older release; pre-stamping - // these to the picked tag's metadata would poison the - // `checkForUpdates` versionCode-parity canary (#542). - // The next periodic check resolves them from the feed. + latestVersion = null, latestAssetName = null, latestAssetUrl = null, diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.kt index 586fa317d..fc36dca3d 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.kt @@ -72,9 +72,6 @@ internal class DeeplTranslator( ) } - // Surface `message` whenever it's present alongside a missing / - // empty translations array — DeepL sometimes returns - // `{"translations":[], "message":"..."}` instead of an HTTP error. val translations = root["translations"]?.jsonArray val errorMessage = root["message"]?.jsonPrimitive?.content if (translations.isNullOrEmpty()) { diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt index e2c5eeb37..2ad5ff35f 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt @@ -9,25 +9,11 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive import zed.rainxch.details.domain.model.TranslationResult -/** - * Hits Google's undocumented `translate_a/single` endpoint. Works - * everywhere Google does, no credentials. May rate-limit or break - * without notice; Youdao is the escape hatch. - * - * Uses POST (form-encoded body) instead of GET: the repository chunks - * on `String.length`, but URL encoding a non-ASCII character — CJK, - * Arabic, etc. — expands ~3× for UTF-8 and ×3 again for percent - * encoding (roughly 9×). A 4500-char CJK chunk would produce a ~40 KB - * URL, well past most HTTP stacks' ~8 KB cap. POST bodies have no - * such limit. - */ internal class GoogleTranslator( private val httpClient: () -> HttpClient, private val json: Json, ) : Translator { - // POST body — bounded by server-side payload limits, not URL - // length. 4500 chars stays well within Google's accepted payload - // even for maximum-expansion CJK text. + override val maxChunkSize: Int = 4500 override suspend fun translate( diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.kt index 3e53f6236..9cc1275f0 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.kt @@ -48,9 +48,7 @@ internal class LibreTranslator( val body = response.bodyAsText() if (!response.status.isSuccess()) { - // Try to parse the JSON error envelope; fall through to raw - // body when the upstream returned HTML / plain text (proxy - // pages, WAF blocks, misconfigured self-host). + val message = runCatching { json.parseToJsonElement(body).jsonObject["error"]?.jsonPrimitive?.content }.getOrNull() diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/Translator.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/Translator.kt index cefd1b298..40b472f7f 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/Translator.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/Translator.kt @@ -2,12 +2,6 @@ package zed.rainxch.details.data.translation import zed.rainxch.details.domain.model.TranslationResult -/** - * Single-shot translator for a chunk of already-sized text. Chunking, - * caching and result-joining stay in the repository layer so each - * provider implementation only has to answer the question "translate - * this string and tell me what language it was in." - */ internal interface Translator { suspend fun translate( text: String, @@ -15,17 +9,7 @@ internal interface Translator { sourceLanguage: String, ): TranslationResult - /** - * Rough per-chunk upper bound for [text] length, in characters. - * Used by the repository's chunker to avoid tripping provider - * limits (Google's GET query length, Youdao's POST body length). - */ val maxChunkSize: Int } -/** - * Raised when the selected provider isn't configured (e.g. Youdao - * selected but `appKey` missing). UI surfaces this as "provider not - * configured" rather than a generic network error. - */ internal class TranslationProviderNotConfiguredException(message: String) : RuntimeException(message) diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt index a606b7786..b51a6fe4b 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt @@ -15,19 +15,6 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import zed.rainxch.details.domain.model.TranslationResult -/** - * Hits Youdao Translation Open API v3 (`openapi.youdao.com/api`). - * Directly accessible from mainland China — the reason this provider - * exists (see issue #429). Requires user-supplied `appKey`/`appSecret` - * from Youdao's developer portal; missing credentials throw - * [TranslationProviderNotConfiguredException] up to the UI. - * - * Signing: v3 uses - * sign = sha256(appKey + input + salt + curtime + appSecret) - * where `input` is the query truncated to first-10 + length + last-10 - * for strings longer than 20 characters. - * See https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html. - */ @OptIn(ExperimentalTime::class, ExperimentalUuidApi::class) internal class YoudaoTranslator( private val httpClient: () -> HttpClient, @@ -35,8 +22,7 @@ internal class YoudaoTranslator( private val appKey: String, private val appSecret: String, ) : Translator { - // POST body — Youdao accepts up to 5000 chars per call. Leave a - // little room for URL-encoding inflation. + override val maxChunkSize: Int = 4500 override suspend fun translate( @@ -87,8 +73,6 @@ internal class YoudaoTranslator( ?.joinToString("\n") { it.jsonPrimitive.content } .orEmpty() - // `l` is "2" — e.g. "en2zh-CHS". First half is the - // auto-detected source language when `from=auto` was requested. val detected = root["l"] ?.jsonPrimitive @@ -104,8 +88,7 @@ internal class YoudaoTranslator( } private fun buildSignInput(q: String): String { - // Youdao's documented truncation rule for the signed `input`: - // if q.length > 20 use first 10 + q.length + last 10, else use q. + return if (q.length <= 20) { q } else { @@ -124,12 +107,6 @@ internal class YoudaoTranslator( } } - /** - * Translate Google-style BCP-47 codes (the rest of the app uses - * these) to Youdao's expected language codes. Anything we don't - * know passes through — if it's wrong Youdao will respond with - * errorCode 102 and the caller surfaces the error. - */ private fun mapLanguageCode(code: String): String = when (code.lowercase()) { "auto", "" -> "auto" diff --git a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt index d73a6befe..6ab6aec00 100644 --- a/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt +++ b/feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/utils/preprocessMarkdown.kt @@ -42,10 +42,6 @@ fun preprocessMarkdown( (lower.contains("/badge") && isSvgUrl(lower)) } - // SVGs are no longer skipped wholesale — Coil's SVG decoder now - // handles them (registered in the App composable). Only known badge - // / shields services stay skipped because they're noise even when - // rendered correctly (status badges = clutter on small screens). fun shouldSkipImage(url: String): Boolean = isBadgeUrl(url) fun resolveUrl(path: String): String { @@ -83,13 +79,6 @@ fun preprocessMarkdown( } } - // ======================================================================== - // Phase 0: Handle reference-style markdown definitions and usages - // ======================================================================== - // Reference definitions: [ref-name]: https://example.com/image.svg - // Reference usages: ![alt][ref-name] or [![img-ref]][link-ref] - - // 0a. Parse all reference definitions val refDefinitionRegex = Regex( """^\[([^\]]+)\]:\s*(\S+).*$""", @@ -102,14 +91,12 @@ fun preprocessMarkdown( referenceMap[refName] = url } - // 0b. Identify which references point to SVGs/badges val skipRefNames = referenceMap .filter { (_, url) -> shouldSkipImage(resolveUrl(url)) }.keys - // 0c. Remove reference-style image usages that point to SVGs: ![alt][svg-ref] if (skipRefNames.isNotEmpty()) { processed = processed.replace( @@ -125,7 +112,6 @@ fun preprocessMarkdown( } } - // 0d. Resolve remaining reference-style images to inline format: ![alt][ref] → ![alt](url) processed = processed.replace( Regex("""!\[([^\]]*)\]\[([^\]]+)\]"""), @@ -141,8 +127,6 @@ fun preprocessMarkdown( } } - // 0e. Handle nested badge-as-link patterns: [![badge-ref]][link-ref] - // After 0c strips the inner image, this can leave [**text**][link-ref] or [][link-ref] processed = processed.replace( Regex("""\[(\*\*[^*]*\*\*)\]\[([^\]]+)\]"""), @@ -156,14 +140,13 @@ fun preprocessMarkdown( boldText } } - // Clean empty bracket patterns left from stripped badge images: [][ref] + processed = processed.replace( Regex("""\[\s*\]\[([^\]]+)\]"""), "", ) - // 0f. Handle reference-style links: [text][ref] → [text](url) processed = processed.replace( Regex("""\[([^\]]+)\]\[([^\]]+)\]"""), @@ -171,7 +154,7 @@ fun preprocessMarkdown( val text = match.groupValues[1] val refName = match.groupValues[2].lowercase() val url = referenceMap[refName] - // Don't convert if text looks like it was already an image (starts with !) + if (url != null && !text.startsWith("!")) { "[$text](${resolveUrl(url)})" } else { @@ -179,7 +162,6 @@ fun preprocessMarkdown( } } - // 0g. Remove all reference definitions that were resolved processed = processed.replace( Regex("""^\[([^\]]+)\]:\s*\S+.*$""", RegexOption.MULTILINE), @@ -188,14 +170,6 @@ fun preprocessMarkdown( if (refName in referenceMap) "" else match.value } - // ======================================================================== - // Phase 0.5:

/ normalisation - // ======================================================================== - // Must run BEFORE Phase 1 — once `
` → `\n` and `
` → `\n\n` - // pass over a table cell, the row gets split and our context check - // (`linePrefix.contains('|')`) below would no longer see the pipe. - // Inline / table-cell details flatten to one line; standalone block - // details emit a fenced `ghs-details` block for ExpandableDetails. processed = processed.replace( Regex( @@ -212,9 +186,7 @@ fun preprocessMarkdown( val mustFlatten = isInline || isInTableCell if (mustFlatten) { - // Collapse any whitespace (incl. embedded HTML tags - // that would otherwise expand to newlines) to single - // spaces so the result stays on one source line. + val flatBody = body .replace(Regex("""""", RegexOption.IGNORE_CASE), " ") .replace(Regex("""\s+"""), " ") @@ -227,21 +199,13 @@ fun preprocessMarkdown( } } else { val encodedSummary = encodeDetailsSummary(summary) - // Body may contain its own ```fenced``` code blocks. Pick a - // fence delimiter at least one backtick longer than the - // longest run inside the body so nested fences don't - // terminate our wrapper early. + val longestRun = longestBacktickRun(body) val fence = "`".repeat(maxOf(4, longestRun + 1)) "\n\n${fence}ghs-details|$encodedSummary\n$body\n$fence\n\n" } } - // ======================================================================== - // Phase 1: HTML → Markdown conversions - // ======================================================================== - - // 1. Unwrap elements → keep only the fallback processed = processed.replace( Regex( @@ -251,14 +215,13 @@ fun preprocessMarkdown( ) { match -> match.groupValues[1] } - // Also strip orphaned tags (outside ) + processed = processed.replace( Regex("""]*?/?>""", RegexOption.IGNORE_CASE), "", ) - // 2. Unwrap tags that wrap tags — keep the for step 3 processed = processed.replace( Regex( @@ -269,7 +232,6 @@ fun preprocessMarkdown( match.groupValues[1] } - // 3. Convert tags → markdown images (handles multiline img tags) processed = processed.replace( Regex( @@ -298,7 +260,6 @@ fun preprocessMarkdown( } } - // 4. Normalize markdown image URLs (resolve relative, normalize GitHub blob) processed = processed.replace( Regex("""!\[([^\]]*)\]\(([^)]+)\)"""), @@ -314,7 +275,6 @@ fun preprocessMarkdown( } } - // 5. Handle
→ markdown headings for (level in 1..6) { val hashes = "#".repeat(level) processed = @@ -352,7 +311,6 @@ fun preprocessMarkdown( } } - // 7. Convert
and
tags processed = processed.replace( Regex("""""", RegexOption.IGNORE_CASE), @@ -364,8 +322,6 @@ fun preprocessMarkdown( "\n---\n", ) - // 8. Convert inline formatting tags - // / → **text** processed = processed.replace( Regex( @@ -375,7 +331,7 @@ fun preprocessMarkdown( ) { match -> "**${match.groupValues[2]}**" } - // / → *text* + processed = processed.replace( Regex( @@ -385,9 +341,7 @@ fun preprocessMarkdown( ) { match -> "*${match.groupValues[2]}*" } - //
 → ``` fence with language hint.
-    // Must run BEFORE the single-line  rule below — that one would
-    // otherwise grab the inner  and lose the language attribute.
+
     processed =
         processed.replace(
             Regex(
@@ -399,7 +353,7 @@ fun preprocessMarkdown(
             val code = match.groupValues[2]
             "\n```$lang\n$code\n```\n"
         }
-    //  → `text` (single-line only, not 
)
+
     processed =
         processed.replace(
             Regex(
@@ -409,7 +363,7 @@ fun preprocessMarkdown(
         ) { match ->
             "`${match.groupValues[1]}`"
         }
-    // 
X
→ markdown `> ` lines. + processed = processed.replace( Regex( @@ -420,7 +374,7 @@ fun preprocessMarkdown( val body = match.groupValues[1].trim() body.lineSequence().joinToString("\n") { "> $it" } } - // / / → ~~text~~ + processed = processed.replace( Regex( @@ -431,7 +385,6 @@ fun preprocessMarkdown( "~~${match.groupValues[2]}~~" } - // 9. Convert
text → [text](url) (non-image links) processed = processed.replace( Regex( @@ -449,7 +402,6 @@ fun preprocessMarkdown( } } - // 10. → `text` processed = processed.replace( Regex( @@ -460,8 +412,6 @@ fun preprocessMarkdown( "`${match.groupValues[1]}`" } - // 11. Strip remaining wrapper tags (keep content) - //
tags processed = processed.replace( Regex("""]*?>\s*""", RegexOption.IGNORE_CASE), @@ -472,7 +422,7 @@ fun preprocessMarkdown( Regex("""
\s*""", RegexOption.IGNORE_CASE), "\n\n", ) - //

/

+ processed = processed.replace( Regex("""]*?>""", RegexOption.IGNORE_CASE), @@ -483,8 +433,7 @@ fun preprocessMarkdown( Regex("""

""", RegexOption.IGNORE_CASE), "\n", ) - // Handle bare/stripped
without inner — keep - // contents as plain markdown. + processed = processed.replace( Regex("""]*?>""", RegexOption.IGNORE_CASE), @@ -499,11 +448,7 @@ fun preprocessMarkdown( ) { match -> "**${match.groupValues[1].trim()}**\n" } - // X / X → Unicode superscript/subscript chars - // where mappable (digits + operators + a few letters). Falls back - // to the literal char when no Unicode codepoint exists — markdown - // lib doesn't expose inline BaselineShift, so this is the best we - // can do without a custom text span. + processed = processed.replace( Regex("""]*>(.*?)""", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)), @@ -516,13 +461,13 @@ fun preprocessMarkdown( ) { match -> match.groupValues[1].map { SUBSCRIPTS[it] ?: it }.joinToString("") } - // — strip tags, keep content + processed = processed.replace( Regex("""]*?>""", RegexOption.IGNORE_CASE), "", ) - // Strip other common straggler HTML tags + processed = processed.replace( Regex( @@ -532,11 +477,8 @@ fun preprocessMarkdown( "\n", ) - // 12. Decode HTML entities. Named entity table covers the long tail - // of READMEs (©, ™, ‘curly quotes’, em/en-dash, ellipsis, - // French/Spanish quotation marks, currency symbols). HTML_ENTITIES.forEach { (entity, char) -> processed = processed.replace(entity, char) } - // Numeric HTML entities (decimal): &#NNN; → char. + processed = processed.replace(Regex("""&#(\d+);""")) { match -> val code = match.groupValues[1].toIntOrNull() @@ -546,7 +488,7 @@ fun preprocessMarkdown( match.value } } - // Numeric HTML entities (hex): &#xHHHH; → char. + processed = processed.replace(Regex("""&#x([0-9A-Fa-f]+);""")) { match -> val code = match.groupValues[1].toIntOrNull(16) @@ -557,7 +499,6 @@ fun preprocessMarkdown( } } - // 13. Clean up empty

tags and excess newlines processed = processed.replace( Regex("""]*?>\s*

""", RegexOption.IGNORE_CASE), @@ -569,21 +510,14 @@ fun preprocessMarkdown( "\n\n", ) - // 14. Clean up orphaned markdown link fragments processed = processed.replace( Regex("""^\]\([^)]+\)""", RegexOption.MULTILINE), "", ) - // 15. Replace GitHub emoji shortcodes (:rocket: → 🚀). Skips fenced - // code blocks; inline `:foo:` patterns inside `` `code` `` are - // rare enough not to warrant deeper tokenisation. processed = zed.rainxch.core.domain.util.EmojiShortcodes.render(processed) - // 16. Join consecutive image-only lines into a single paragraph so - // badge galleries (Play Store + GitHub buttons etc.) render - // in a row instead of stacking one per line. processed = joinAdjacentImageLines(processed) return processed.trim() @@ -604,8 +538,7 @@ private fun longestBacktickRun(text: String): Int { } private fun encodeDetailsSummary(text: String): String { - // URL-encode special chars to keep summary on one line inside the - // fence info string. Decoder mirror lives in the codeFence slot. + val safe = StringBuilder() text.forEach { c -> when (c) { @@ -628,7 +561,7 @@ private fun joinAdjacentImageLines(content: String): String { while (i < lines.size) { val line = lines[i] if (imageOnlyLine.matches(line)) { - // Greedily collect adjacent image-only lines. + val group = StringBuilder(line.trim()) var j = i + 1 while (j < lines.size && imageOnlyLine.matches(lines[j])) { @@ -647,25 +580,20 @@ private fun joinAdjacentImageLines(content: String): String { return out.toString() } -// ============================================================================ -// Lookup tables for HTML entity decoding + sub/sup Unicode mapping. -// ============================================================================ - private val HTML_ENTITIES: Map = mapOf( - // Core 5 (must come first; processor relies on them being decoded - // before any subsequent regex that operates on raw `<`/`>`). + "&" to "&", "<" to "<", ">" to ">", """ to "\"", "'" to "'", "'" to "'", - // Whitespace + " " to " ", " " to " ", " " to " ", " " to " ", - // Punctuation / typography + "…" to "…", "—" to "—", "–" to "–", @@ -681,7 +609,7 @@ private val HTML_ENTITIES: Map = mapOf( "·" to "·", "§" to "§", "¶" to "¶", - // Math / arrows + "×" to "×", "÷" to "÷", "±" to "±", @@ -700,11 +628,11 @@ private val HTML_ENTITIES: Map = mapOf( "↔" to "↔", "⇐" to "⇐", "⇒" to "⇒", - // Legal / brand + "©" to "©", "®" to "®", "™" to "™", - // Currency + "€" to "€", "£" to "£", "¥" to "¥", diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/ApkValidationResult.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/ApkValidationResult.kt index 05022cb95..677d66380 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/ApkValidationResult.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/ApkValidationResult.kt @@ -3,15 +3,13 @@ package zed.rainxch.details.domain.model import zed.rainxch.core.domain.model.ApkPackageInfo sealed interface ApkValidationResult { - /** APK is valid and ready to install. */ + data class Valid( val apkInfo: ApkPackageInfo, ) : ApkValidationResult - /** Could not extract package information from the APK. */ data object ExtractionFailed : ApkValidationResult - /** Package name in the APK does not match the currently installed app. */ data class PackageMismatch( val apkPackageName: String, val installedPackageName: String, diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/FingerprintCheckResult.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/FingerprintCheckResult.kt index 6f4716208..4f0e2ac21 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/FingerprintCheckResult.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/FingerprintCheckResult.kt @@ -1,10 +1,9 @@ package zed.rainxch.details.domain.model sealed interface FingerprintCheckResult { - /** Fingerprint matches or no prior fingerprint is recorded. */ + data object Ok : FingerprintCheckResult - /** Signing key has changed compared to the previously installed version. */ data class Mismatch( val expectedFingerprint: String, val actualFingerprint: String, diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt index 01f540b7f..6a93aee5d 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt @@ -14,11 +14,6 @@ data class SaveInstalledAppParams( val isFavourite: Boolean, val siblingAssetCount: Int, val pickedAssetIndex: Int?, - // Path to the parked APK on disk when this row is being saved as a - // pending install (e.g. system installer was launched but the user - // hasn't accepted yet). The apps row uses this to drive its one-tap - // Install retry button and to extract a real app icon while the - // package isn't yet on the system. `null` for genuine completed - // installs and for non-pending saves. + val pendingInstallFilePath: String? = null, ) diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/AttestationVerifier.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/AttestationVerifier.kt index 3fac96662..ef0606659 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/AttestationVerifier.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/AttestationVerifier.kt @@ -1,18 +1,7 @@ package zed.rainxch.details.domain.system -/** - * Verifies build attestations for downloaded assets using GitHub's - * supply-chain security API. - */ interface AttestationVerifier { - /** - * Computes the SHA-256 digest of [filePath] and checks whether - * the repository [owner]/[repoName] has a matching attestation. - * - * @return [VerificationResult.Verified] if a valid attestation exists, - * [VerificationResult.Unverified] if no matching attestation was found, - * [VerificationResult.Error] if the check could not be completed. - */ + suspend fun verify( owner: String, repoName: String, diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/InstallationManager.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/InstallationManager.kt index d1e1e2435..c47015a59 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/InstallationManager.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/system/InstallationManager.kt @@ -8,37 +8,17 @@ import zed.rainxch.details.domain.model.FingerprintCheckResult import zed.rainxch.details.domain.model.SaveInstalledAppParams import zed.rainxch.details.domain.model.UpdateInstalledAppParams -/** - * Encapsulates APK validation, fingerprint checking, and - * installed-app database persistence so the ViewModel stays thin. - */ interface InstallationManager { - /** - * Extracts [ApkPackageInfo] from [filePath] and validates it. - * On an update, verifies the package name matches [trackedPackageName]. - */ + suspend fun validateApk( filePath: String, isUpdate: Boolean, trackedPackageName: String?, ): ApkValidationResult - /** - * Checks whether the signing fingerprint of [apkInfo] matches - * the fingerprint previously recorded for the same package. - */ suspend fun checkSigningFingerprint(apkInfo: ApkPackageInfo): FingerprintCheckResult - /** - * Saves a freshly installed app to the database and optionally - * updates the favourite install status. - * - * @return the reloaded [InstalledApp], or `null` on failure. - */ suspend fun saveNewInstalledApp(params: SaveInstalledAppParams): InstalledApp? - /** - * Updates the version metadata of an already-tracked app. - */ suspend fun updateInstalledAppVersion(params: UpdateInstalledAppParams) } diff --git a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/util/VersionHelper.kt b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/util/VersionHelper.kt index 22a346622..69214f646 100644 --- a/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/util/VersionHelper.kt +++ b/feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/util/VersionHelper.kt @@ -3,42 +3,9 @@ package zed.rainxch.details.domain.util import zed.rainxch.core.domain.model.GithubRelease import zed.rainxch.core.domain.util.VersionMath -/** - * Thin wrapper that defers all comparisons to - * [zed.rainxch.core.domain.util.VersionMath]. Kept as a separate type - * because the details feature also needs a release-list-aware - * downgrade check (see [isDowngradeVersion]) — that fallback is - * specific to the install flow on the details screen, not something - * the rest of the app needs. - */ object VersionHelper { fun normalizeVersion(version: String?): String = VersionMath.normalizeVersion(version) - /** - * Returns `true` when installing [candidate] over [current] would be - * a downgrade. - * - * Strategy: - * 1. Trust [VersionMath.compareVersions] when both inputs have a - * recognisable versioning scheme (SemVer / CalVer). The sign of - * the comparator is authoritative — list-position is too - * unreliable since GitHub's release ordering follows - * `published_at`, and maintainers can reorder by republishing - * or backdating a release. - * 2. Fall back to list-index ordering only when at least one input - * has no parseable scheme (commit-hash tags, ad-hoc strings). - * The release feed is newest-first, so a candidate that appears - * later in the list is older. - * 3. As a last resort, when neither lookup nor scheme detection - * yields an answer, fall through to [VersionMath.compareVersions] - * (which itself falls back to lexicographic comparison). - * - * Cross-references for the install flow caller behaviour: - * - `DetailsViewModel.install()` skips this check entirely when - * `normalizeVersion(candidate) == normalizeVersion(current)`. - * - The result gates [DowngradeWarning] so a `false` here proceeds - * straight to install. - */ fun isDowngradeVersion( candidate: String, current: String, @@ -74,11 +41,6 @@ object VersionHelper { return cmp < 0 } - /** - * Three-way comparison delegating to [VersionMath.compareVersions]. - * Kept on this surface so existing call sites don't have to learn - * the new helper's name. - */ fun compareSemanticVersions( a: String, b: String, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt index 06d3f81db..7f7f24836 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt @@ -113,74 +113,28 @@ sealed interface DetailsAction { data object DismissLanguagePicker : DetailsAction - // show release asset picker data class SelectDownloadAsset( val release: GithubAsset, ) : DetailsAction data object ToggleReleaseAssetsPicker : DetailsAction - /** - * Clears the user's preferred variant pin for the currently-tracked - * app. Falls back to the platform auto-picker on subsequent updates. - * Triggered by the "Unpin variant" affordance in the asset picker - * sheet. - */ data object UnpinPreferredVariant : DetailsAction - /** - * Flips the per-app `includePreReleases` flag. Exposed as the - * inline channel toggle on Details so users can opt in/out of - * beta updates without digging into the apps advanced settings - * sheet (GitHub-Store release UX #2). - */ data object ToggleIncludeBetas : DetailsAction - /** - * Switches the currently-tracked app from a pre-release to the - * latest stable release. Selects the stable release and - * initiates the install flow on it (GitHub-Store release UX #3). - */ data object SwitchToStable : DetailsAction - /** - * Opens the APK Inspect bottom sheet. The ViewModel resolves the - * APK source automatically — installed package wins over parked - * file when both exist (installed manifest is the authoritative - * source on what's actually on the device). - */ data object OnInspectApk : DetailsAction - /** Closes the APK Inspect bottom sheet. */ data object OnDismissApkInspect : DetailsAction - /** - * Acknowledges the inspect-button discoverability coachmark — fired - * either when the coachmark is tapped/dismissed or when the user - * opens the inspect sheet for the first time. Persists so the - * coachmark only ever shows once. - */ data object OnAcknowledgeApkInspectCoachmark : DetailsAction - /** - * Acknowledges the release-channel chip coachmark. Fired on - * tap/dismiss or when the user toggles the channel chip itself. - * Persists so the coachmark only ever shows once. - */ data object OnAcknowledgeChannelChipCoachmark : DetailsAction - /** - * Flip the "Show all platforms" picker setting (persisted globally). - * Carries the explicit target value so rapid back-and-forth toggles - * don't race against a stale read of the in-memory state. - */ data class OnToggleShowAllPlatforms(val enabled: Boolean) : DetailsAction - /** - * Download a non-current-platform asset for transfer to another - * device. Routes to the user's browser so the file lands in their - * normal Downloads folder. - */ data class OnDownloadForTransfer( val assetUrl: String, ) : DetailsAction diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index ccf519d57..41e411ab4 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -275,7 +275,6 @@ fun DetailsRoot( ) } - // Signing key changed warning dialog state.signingKeyWarning?.let { warning -> AlertDialog( onDismissRequest = { @@ -322,7 +321,6 @@ fun DetailsRoot( ) } - // Uninstall confirmation dialog if (state.showUninstallConfirmation) { val appName = state.installedApp?.appName ?: "" AlertDialog( @@ -523,13 +521,6 @@ fun DetailsScreen( } val pullEnabled = remember { isPullToRefreshSupported() } - // Gutter scroll forwarding is a desktop-only UX polish - // (mouse-wheel-in-side-margins → scrolls content column). - // On Android the outer scrollable fought the inner - // LazyColumn's own scroll handler — both pointing at the - // same `listState` doubled-up gesture handling and the - // touch scroll froze. Issue tracked under content-width - // PR follow-up. Keep enabled = isDesktop. val isDesktop = remember { getPlatform() != Platform.ANDROID } Box( modifier = Modifier @@ -537,23 +528,12 @@ fun DetailsScreen( .scrollable( state = listState, orientation = Orientation.Vertical, - // LazyColumn's internal scrollable uses - // reverseDirection=true (vertical, default - // layout direction). Matching that here makes - // the wheel-direction sign convention agree - // — otherwise wheel-up at the top jumps to - // the bottom and vice versa, because parent - // and child interpret the same pointer delta - // with opposite signs. + reverseDirection = true, enabled = isDesktop, ) .onSizeChanged { size -> - // Layout-phase write; cheaper than BoxWithConstraints - // which subcomposes during the measure pass. Setting - // a state var here recomposes only the consumers that - // read it (the about/whatsNew sections), not the - // entire Scaffold subtree. + val newHeight = with(density) { size.height.toDp() } if (newHeight != containerHeightDp) containerHeightDp = newHeight }, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt index aa6142c84..2db05e3ff 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt @@ -27,10 +27,10 @@ data class DetailsState( val errorMessage: String? = null, val userProfile: GithubUserProfile? = null, val repository: GithubRepoSummary? = null, - // state for assets + val primaryAsset: GithubAsset? = null, val installableAssets: List = emptyList(), - // state for releases + val selectedRelease: GithubRelease? = null, val allReleases: List = emptyList(), val releasesLoadFailed: Boolean = false, @@ -57,23 +57,14 @@ data class DetailsState( val isAppManagerAvailable: Boolean = false, val isAppManagerEnabled: Boolean = false, val installedApp: InstalledApp? = null, - /** - * All apps tracked for this repository. For single-app repos this - * contains at most one element (same as [installedApp]). For - * monorepos it may contain multiple entries with different package - * names. [installedApp] is the "primary" — the one whose asset - * filter matches the currently selected asset, or the first. - */ + val installedApps: List = emptyList(), val isFavourite: Boolean = false, val isStarred: Boolean = false, val isTrackingApp: Boolean = false, val isAboutExpanded: Boolean = false, val isWhatsNewExpanded: Boolean = false, - // Measured intrinsic heights of the rendered markdown blocks, hoisted - // out of the composable so LazyColumn item disposal/recompose doesn't - // re-trigger the measure → clip → reflow loop that snapped scroll - // position to the section start. + val aboutMeasuredHeightPx: Float? = null, val whatsNewMeasuredHeightPx: Float? = null, val aboutTranslation: TranslationState = TranslationState(), @@ -89,63 +80,25 @@ data class DetailsState( val showUninstallConfirmation: Boolean = false, val showUnlinkConfirmation: Boolean = false, val attestationStatus: AttestationStatus = AttestationStatus.UNCHECKED, - /** - * Days since the most recent stable release when the project is - * actively shipping pre-releases on top of it. `null` means - * either healthy (recent stable) or no applicable signal - * (project has no stable releases at all). Set by the ViewModel - * from `latestStable.publishedAt` vs `Clock.now()` when releases - * load. See release UX #6. - */ + val stalledStableSinceDays: Int? = null, - /** - * Concatenated release notes for every release newer than the - * user's `installedApp.installedVersion`, most-recent-first. - * Populated when the user is tracking the app and at least one - * newer release exists. Null when there's no installed version - * or no newer releases. See release UX #4. - */ + val mergedChangelog: String? = null, - /** - * Release tag for the head of [mergedChangelog] (the version the - * user would jump from). Used to title the merged section as - * "What's changed since v1.2.3". - */ + val mergedChangelogBaseTag: String? = null, - /** - * Whether [latestStableRelease] has at least one asset that the - * platform installer can handle. Computed by the ViewModel - * whenever `allReleases` changes — we can't compute it here - * because the installer's per-platform asset-extension policy - * lives outside the data model. Gates [canSwitchToStable] so - * the rollback chip never advertises an action that would - * silently no-op for releases that ship only source tarballs. - */ + val latestStableHasInstallableAsset: Boolean = false, - /** APK inspection result currently driving the inspect bottom sheet. */ + val apkInspection: ApkInspection? = null, - /** Whether the inspect bottom sheet is on screen. */ + val isApkInspectSheetVisible: Boolean = false, - /** Loading state for the inspect sheet — set while the inspector runs. */ + val isApkInspectLoading: Boolean = false, - /** - * One-shot flag from DataStore — `false` until the user has seen - * the discoverability coachmark for the inspect button. Drives the - * pulse + tooltip animation in the install button row. - */ + val isApkInspectCoachmarkPending: Boolean = false, - /** - * One-shot flag — `false` until the user has seen the - * release-channel coachmark. Drives the pulse + tooltip on the - * `ChannelChip` so users discover the per-app channel toggle. - */ + val isChannelChipCoachmarkPending: Boolean = false, - /** - * Mirrors `TweaksRepository.getShowAllPlatforms()`. When true the - * release-assets picker lists installers for every OS (grouped by - * section); the install button still operates on the current - * platform's primary asset. - */ + val showAllPlatforms: Boolean = false, ) { val filteredReleases: List @@ -156,26 +109,12 @@ data class DetailsState( ReleaseCategory.ALL -> allReleases } - /** - * Most recent non-pre-release release, or `null` when the - * project has no stable releases in the current window. Drives - * the "Switch to stable vX.Y.Z" rollback action. - */ val latestStableRelease: GithubRelease? get() = allReleases .filter { !it.isEffectivelyPreRelease() } .maxByOrNull { it.publishedAt } - /** - * True when the install button should expose a "switch to - * stable" rollback affordance: the user is tracking this app, - * is currently on a release that's effectively a pre-release, - * a distinct stable release exists, AND that stable release has - * at least one installable asset on the current platform. The - * handler (`DetailsAction.SwitchToStable`) selects the stable - * release and invokes the normal install path. - */ val canSwitchToStable: Boolean get() { val app = installedApp ?: return false @@ -185,21 +124,10 @@ data class DetailsState( allReleases.firstOrNull { VersionMath.isSameVersion(it.tagName, app.installedVersion) } ?.isEffectivelyPreRelease() == true if (!installedIsPreRelease) return false - // Don't offer the button if the stable release IS the - // one the user has already (same version, ignoring tag-prefix - // drift like "v" vs "" or "release-" vs ""). + return !VersionMath.isSameVersion(stable.tagName, app.installedVersion) } - /** - * True when the currently-tracked app has a *parked* install file - * that matches the user's current selection (release tag + asset - * name). The install button can short-circuit the download phase - * and dispatch the dialog/install flow on the parked file directly. - * - * This is the data-layer match — the VM also re-checks the file - * exists on disk before actually using it (in [parkedFilePathIfMatches]). - */ val isPendingInstallReady: Boolean get() { val app = installedApp ?: return false diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index abe7d4ffe..f549420a5 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -105,9 +105,7 @@ class DetailsViewModel( private val repositoryId: Long, private val ownerParam: String, private val repoParam: String, - // Non-null for Forgejo / Codeberg / custom-forge repos. Routes all - // DetailsRepository calls through the Forgejo API client instead of - // the GitHub-backed default path. + private val sourceHostParam: String?, private val detailsRepository: DetailsRepository, private val downloader: Downloader, @@ -189,9 +187,7 @@ class DetailsViewModel( logger.debug("Unlinking externally-imported app: $packageName") viewModelScope.launch { try { - // installed_apps + external_links must move together so the - // next scan re-proposes a match instead of treating the row - // as a healthy tracked app on a stale link. + installedAppsRepository.executeInTransaction { externalImportRepository.unlink(packageName) installedAppsRepository.deleteInstalledApp(packageName) @@ -484,9 +480,7 @@ class DetailsViewModel( } DetailsAction.ToggleIncludeBetas -> { - // Tapping the chip is the strongest possible signal that - // the user has discovered the toggle. No need to keep - // pulsing it after that. + acknowledgeChannelChipCoachmark() toggleIncludeBetas() } @@ -526,11 +520,7 @@ class DetailsViewModel( } is DetailsAction.OnDownloadForTransfer -> { - // Cross-platform assets land here when the user picks an - // installer for a different OS. Browser handles the - // actual save-to-Downloads — keeps install plumbing - // unchanged and matches existing "open external link" - // flows on Details. + helper.openUrl(action.assetUrl) { err -> logger.warn("Open transfer download failed: $err") } @@ -538,14 +528,6 @@ class DetailsViewModel( } } - /** - * Resolves the right APK source and runs [ApkInspector]. Installed - * package wins over a parked file when both exist — a successful - * install means the manifest on the system is the authoritative - * description of what's actually running, even if the bytes that - * produced it are still parked. Falls back to the parked file path - * for pre-install inspections. - */ private fun openApkInspectSheet() { val installed = _state.value.installedApp val parkedPath = installed?.pendingInstallFilePath @@ -581,9 +563,7 @@ class DetailsViewModel( apkInspection = inspection, ) } - // Opening the sheet implicitly satisfies the discoverability - // coachmark — no need to keep nudging the user about a - // feature they've now used. + acknowledgeApkInspectCoachmark() } } @@ -600,12 +580,7 @@ class DetailsViewModel( } private fun acknowledgeChannelChipCoachmark() { - // Persist unconditionally — `state.update` with the same value is a - // no-op, and `setChannelChipCoachmarkShown(true)` is idempotent. - // Skipping persistence when in-memory `pending` is already false - // (e.g., toggle + dismiss race) leaves the DataStore flag at false - // if the very first persist failed, so the coachmark would - // re-appear on next launch. + _state.update { it.copy(isChannelChipCoachmarkPending = false) } viewModelScope.launch { runCatching { tweaksRepository.setChannelChipCoachmarkShown(true) } @@ -615,12 +590,6 @@ class DetailsViewModel( } } - /** - * Derived signals surfaced in the Details UX for pre-release - * handling (release UX #4 and #6). Computed once per release-list - * load and re-used across the two call sites that update state - * with a fresh `allReleases`. - */ private data class ReleaseInsights( val stalledStableSinceDays: Int?, val mergedChangelog: String?, @@ -633,11 +602,7 @@ class DetailsViewModel( allReleases: List, installedApp: InstalledApp?, ): ReleaseInsights { - // Merged "What's changed since v…": concatenate release notes - // for every release strictly newer than the installed tag, - // most-recent-first. Mirrors what app stores do when the user - // skips versions between updates — they deserve to see every - // intermediate changelog, not just the head one. + val (merged, mergedBase) = if (installedApp != null && allReleases.size > 1) { val installedTag = installedApp.installedVersion @@ -665,11 +630,6 @@ class DetailsViewModel( .filter { !it.isEffectivelyPreRelease() } .maxByOrNull { it.publishedAt } - // Stalled-project warning: the project has at least one stable - // release, has shipped pre-releases on top of it, and the last - // stable is older than [STALLED_STABLE_THRESHOLD_DAYS]. That's - // the "beta spiral with no stabilisation" signal that warrants - // a heads-up before the user opts into betas. val stalledDays: Int? = run { val stable = latestStable ?: return@run null @@ -707,13 +667,6 @@ class DetailsViewModel( } } - /** - * Flips the per-app `includePreReleases` flag via - * [InstalledAppsRepository.setIncludePreReleases]. Kicks off a - * fresh `checkForUpdates` so the new channel takes effect - * immediately on-screen instead of waiting for the next - * periodic cycle. - */ private fun toggleIncludeBetas() { val app = _state.value.installedApp ?: return val newValue = !app.includePreReleases @@ -723,9 +676,7 @@ class DetailsViewModel( packageName = app.packageName, enabled = newValue, ) - // Re-validate against the new channel immediately so - // the user sees the result of the toggle in the next - // frame (the DB observer will also refresh state). + installedAppsRepository.checkForUpdates(app.packageName) } catch (e: CancellationException) { throw e @@ -735,22 +686,9 @@ class DetailsViewModel( } } - /** - * Switches the currently-tracked app to the latest stable - * release: selects it as the picked release, and triggers the - * normal install flow. Reuses the existing `InstallPrimary` - * path so downgrade warnings, signing-key checks, and asset - * picking all kick in exactly as they do for a manual version - * selection. - */ private fun switchToStable() { val stable = _state.value.latestStableRelease ?: return - // Defence in depth: the chip should already be hidden when the - // stable release ships nothing the platform installer can - // handle, but a stale state could still drive us here. Resolve - // the primary asset up front and bail before the dispatch chain - // would otherwise reach `install()` with `primaryAsset = null` - // and silently no-op. + val (_, primary) = recomputeAssetsForRelease(stable, _state.value.installedApp) if (primary == null) { logger.warn( @@ -762,20 +700,6 @@ class DetailsViewModel( onAction(DetailsAction.InstallPrimary) } - /** - * Persists the multi-layer fingerprint of [picked] when: - * - the app is already tracked (otherwise there's no row to update — - * the link flow will derive the fingerprint at install time) - * - the picked asset has a non-null fingerprint (single-asset releases - * and unparseable filenames return null) - * - the new fingerprint differs from what's currently stored, OR the - * stale flag is set (re-picking the same variant after a stale event - * must clear the flag) - * - * Emits a one-time "remembered" toast when the app had no fingerprint - * before this pick — that's the user's first time pinning, and the - * implicit behaviour deserves to be made explicit. - */ private fun persistPreferredVariantOnPick(picked: GithubAsset) { val installedApp = _state.value.installedApp ?: return val installable = _state.value.installableAssets @@ -805,10 +729,6 @@ class DetailsViewModel( pickedIndex == installedApp.pickedAssetIndex && newSiblingCount == installedApp.pickedAssetSiblingCount - // Treat the app as "previously unpinned" only when *all* identity - // layers are blank — otherwise we'd nag every time the user - // re-picked the same variant after the resolver populated the - // legacy tail field. val isFirstPin = currentVariant.isNullOrBlank() && currentTokens.isNullOrBlank() && @@ -869,17 +789,6 @@ class DetailsViewModel( } } - /** - * One-shot eligibility check for the APK Inspect coachmark. The - * coachmark may *only* fire if the user opened this Details screen - * with the app already genuinely installed — never as a side effect - * of an install completing during the current session. Otherwise - * the pulse would render at the exact moment the system install - * prompt is up, which is the user's peak-attention frame. - */ - // Reactively flips `isCurrentUserOwner` whenever either the signed-in - // user changes (login/logout/switch-account) or the loaded repository - // owner login arrives. Avoids touching every repo-assignment site. private fun observeCurrentUserForBadge() { viewModelScope.launch { combine( @@ -902,13 +811,7 @@ class DetailsViewModel( runCatching { tweaksRepository.getApkInspectCoachmarkShown().first() } .getOrDefault(true) if (alreadyShown) return@launch - // Wait for `loadInitial` to settle. The first non-loading - // emission carries the authoritative `installedApp` for the - // app the user is viewing. If it's null (or pending) at - // that frame, this screen instance is not eligible — we - // never enable the coachmark for the rest of the session, - // even if an install completes here. The user will see it - // on their next visit instead. + val firstStable = _state.first { !it.isLoading } val installedAtOpen = firstStable.installedApp?.isReallyInstalled() == true @@ -931,10 +834,7 @@ class DetailsViewModel( runCatching { tweaksRepository.getChannelChipCoachmarkShown().first() } .getOrDefault(true) if (alreadyShown) return@launch - // ChannelChip only renders when the app is tracked - // (`installedApp != null` in `ReleaseChannel.releaseChannel`). - // No point pulsing a non-existent chip — defer to a future - // visit where the user actually has the app installed. + val firstStable = _state.first { !it.isLoading } if (firstStable.installedApp == null) return@launch _state.update { it.copy(isChannelChipCoachmarkPending = true) } @@ -955,11 +855,7 @@ class DetailsViewModel( defaultBranch = repo.defaultBranch, sourceHost = sourceHostParam, ) - // Prefer a release that matches the user's previous category. - // Only fall back to the generic "first stable, else first" rule - // when no release exists in that category — in which case reset - // the category too so the UI doesn't end up with a category - // selected but no matching release. + val byPrevCategory = when (prevCategory) { ReleaseCategory.STABLE -> releases.firstOrNull { !it.isEffectivelyPreRelease() } ReleaseCategory.PRE_RELEASE -> releases.firstOrNull { it.isEffectivelyPreRelease() } @@ -968,11 +864,7 @@ class DetailsViewModel( val selected = byPrevCategory ?: releases.firstOrNull { !it.isEffectivelyPreRelease() } ?: releases.firstOrNull() - // When the previous category yields nothing, derive the - // category from the actually-selected release so the - // filter matches what's on screen — otherwise a - // pre-release-only project leaves the user with category - // STABLE and an empty filtered list. + val resolvedCategory = when { byPrevCategory != null -> prevCategory selected?.isEffectivelyPreRelease() == true -> ReleaseCategory.PRE_RELEASE @@ -1004,12 +896,7 @@ class DetailsViewModel( it.copy(isRetryingReleases = false, releasesLoadFailed = true) } } catch (t: Throwable) { - // The detailed cause ("HTTP 403", network error, parse - // failure) only matters for telemetry — for the user, - // "the release list isn't available right now, try - // again in a bit" is the entire signal. Surface the - // friendly message via snackbar; keep the raw cause in - // logs so support / bug reports can still trace it. + logger.warn("Retry failed to load releases: ${t.message}") viewModelScope.launch { _events.send( @@ -1058,22 +945,6 @@ class DetailsViewModel( return installable to primary } - /** - * Pick the "primary" installed app for a repo when multiple - * variants are tracked (e.g. generic + Play-flavored APKs of the - * same project). Earlier code picked `apps.firstOrNull()`, which - * for OSS-DocumentScanner-style multi-flavor repos could pick the - * outdated variant and surface a misleading "Update" CTA even - * when the variant the user is actually running is current. - * Issue #638. - * - * Preference order: - * 1. Variant whose `assetFilterRegex` matches the current - * primaryAsset name — strongest signal that this row owns - * the selected release asset. - * 2. Variant that is up-to-date (`isUpdateAvailable == false`). - * 3. First app — preserves prior behavior for single-variant repos. - */ private fun pickPrimaryInstalledApp( apps: List, primaryAssetName: String?, @@ -1097,21 +968,12 @@ class DetailsViewModel( .getAppsByRepoIdAsFlow(repoId) .distinctUntilChanged() .collect { apps -> - // See [pickPrimaryInstalledApp]. Picks the variant - // that already matches the latest release first, - // so multi-variant projects (e.g. an app shipped - // as both a generic + Play-store-flavored APK) - // don't surface a false "Update" CTA when only one - // of the variants is current. Issue #638. + val primary = pickPrimaryInstalledApp( apps = apps, primaryAssetName = _state.value.primaryAsset?.name, ) - // Recompute merged changelog + stalled signals - // against the new installed version — if the - // user just updated externally, the installed - // tag flips and what they've "missed" changes. val insights = computeReleaseInsights(_state.value.allReleases, primary) _state.update { it.copy( @@ -1609,9 +1471,7 @@ class DetailsViewModel( viewModelScope.launch { try { val ext = warning.pendingAssetName.substringAfterLast('.', "").lowercase() - // Same Android-only serialization rationale as the primary - // install path (`installRelease`): non-Android installers - // never receive the broadcast that releases the gate. + val gatePackageName = if (platform == Platform.ANDROID) warning.pendingApkInfo.packageName else null if (gatePackageName != null) { @@ -1667,29 +1527,6 @@ class DetailsViewModel( } } - /** - * Entry point for "download + install" from the install button. - * - * Hands the actual download off to [downloadOrchestrator] (so it - * survives this screen being destroyed) and then observes the - * orchestrator's state to mirror progress into [DetailsState] and - * to dispatch the install dialog flow when bytes are on disk. - * - * Install policy is decided by installer type: - * - **Shizuku**: [InstallPolicy.AlwaysInstall] — orchestrator - * runs the install in its own scope. The user gets a silent - * install whether they stay on this screen or not. The - * PackageEventReceiver picks up `PACKAGE_REPLACED` and the - * installed-apps DB syncs without further work from the VM. - * - **Regular installer**: [InstallPolicy.InstallWhileForeground] - * — orchestrator parks the file at `AwaitingInstall` and the - * foreground VM (this one) runs the existing dialog flow - * (validation → fingerprint check → installer.install → DB - * save). If the screen leaves before bytes are done, the - * VM's `onCleared` calls [DownloadOrchestrator.downgradeToDeferred], - * the orchestrator notifies the user, and the apps row picks - * up the deferred install. - */ private fun installAsset( downloadUrl: String, assetName: String, @@ -1697,11 +1534,7 @@ class DetailsViewModel( releaseTag: String, isUpdate: Boolean = false, ) { - // Cancel the existing observation job (if any) — but not the - // orchestrator entry itself. A user re-tapping install for a - // different asset should preempt the *previous* observer, not - // the in-flight download (the orchestrator dedupes by - // package name). + currentDownloadJob?.cancel() val packageKey = orchestratorKey() val asset = _state.value.primaryAsset @@ -1712,16 +1545,6 @@ class DetailsViewModel( } currentAssetName = assetName - // ──────────────────────────────────────────────────────── - // SHORT-CIRCUIT: parked file matches what the user picked - // ──────────────────────────────────────────────────────── - // If the user already deferred a download for this exact - // (releaseTag, assetName) pair (e.g. they navigated away - // from Details mid-download, the file got parked, and now - // they're back), skip the orchestrator entirely and - // dispatch the existing install dialog flow on the parked - // file directly. Saves the network round-trip and the - // disk space of a duplicate download. val parkedFilePath = parkedFilePathIfMatches(releaseTag, assetName) if (parkedFilePath != null) { logger.debug("Reusing parked file for $releaseTag / $assetName") @@ -1846,19 +1669,6 @@ class DetailsViewModel( } } - /** - * Returns the path of a parked install file iff the currently-tracked - * app has one AND it represents *this exact* (releaseTag, assetName) - * pair AND the file still exists on disk. - * - * Used as the short-circuit gate in [installAsset] to skip the - * orchestrator round-trip when the bytes are already on disk. - * Returns `null` (= "do a fresh download") in any of these cases: - * - app not tracked - * - no parked file - * - parked file represents a different version or asset - * - parked file no longer exists (manually deleted, etc.) - */ private fun parkedFilePathIfMatches( releaseTag: String, assetName: String, @@ -1869,10 +1679,7 @@ class DetailsViewModel( val parkedAsset = installedApp.pendingInstallAssetName ?: return null if (parkedVersion != releaseTag) return null if (parkedAsset != assetName) return null - // Verify the file still exists. If a user manually cleared - // their downloads dir between parking and re-opening Details, - // the column points at a stale path and we'd hand the - // installer a missing file. + return try { val file = File(parkedPath) if (file.exists() && file.length() > 0) parkedPath else null @@ -1882,18 +1689,6 @@ class DetailsViewModel( } } - /** - * Stable orchestrator key for the currently-displayed app. - * - * Tracked apps key by `packageName` so the apps list and the - * details screen point at the same orchestrator entry. Untracked - * apps (fresh installs) key by `owner/repo` synthetic — real - * package names never contain `/`, so there's no collision risk. - * - * After a fresh install completes, the InstalledApp row is created - * with the real package name; subsequent updates use the real - * key. The synthetic key is one-shot and short-lived. - */ private fun orchestratorKey(): String { val packageName = _state.value.installedApp?.packageName if (packageName != null) return packageName @@ -1902,19 +1697,6 @@ class DetailsViewModel( return "$owner/$name" } - /** - * Subscribes to the orchestrator's entry for [packageKey] and - * mirrors its state into [DetailsState]. When the entry reaches - * [OrchestratorStage.AwaitingInstall] *and* the install policy is - * [InstallPolicy.InstallWhileForeground] (i.e. not the Shizuku - * silent path), kicks off the existing install dialog flow on - * the file path. - * - * Suspends until the entry reaches a terminal state (`Completed`, - * `Cancelled`, `Failed`, or removed from the map). Cancellation - * of the *observer* doesn't cancel the orchestrator — that's the - * whole point. - */ private suspend fun observeOrchestratorEntry( packageKey: String, downloadUrl: String, @@ -1927,9 +1709,7 @@ class DetailsViewModel( var telemetryStartFired = false downloadOrchestrator.observe(packageKey).collect { entry -> if (entry == null) { - // Orchestrator dropped the entry (cancelled or - // dismissed elsewhere). Tear down our local UI state - // and exit the observer. + if (_state.value.downloadStage != DownloadStage.IDLE) { _state.value = _state.value.copy( @@ -1941,11 +1721,6 @@ class DetailsViewModel( return@collect } - // Mirror progress into local state for the UI. Update - // bytes too — the live byte counter is what users see - // when content-length is small enough that the percent - // doesn't tick smoothly. The orchestrator emits both on - // every chunk so the UI gets a continuous update. _state.value = _state.value.copy( downloadProgressPercent = entry.progressPercent, @@ -1955,7 +1730,7 @@ class DetailsViewModel( when (entry.stage) { OrchestratorStage.Queued -> { - // Nothing UI-visible — same as DOWNLOADING placeholder + _state.value = _state.value.copy(downloadStage = DownloadStage.DOWNLOADING) } @@ -1964,9 +1739,7 @@ class DetailsViewModel( } OrchestratorStage.Installing -> { - // Either the orchestrator's bare-install path - // (Shizuku) or our own install fired below. Either - // way, surface the INSTALLING stage. + _state.value = _state.value.copy(downloadStage = DownloadStage.INSTALLING) if (!telemetryStartFired) { @@ -1977,12 +1750,7 @@ class DetailsViewModel( } OrchestratorStage.AwaitingInstall -> { - // Bytes are on disk. For the foreground path - // (regular installer), this is our cue to run - // the existing dialog/validation/install flow. - // For the Shizuku path the orchestrator already - // moved past Installing → Completed before we - // ever see AwaitingInstall (it doesn't park). + if (installFired) return@collect installFired = true val filePath = entry.filePath ?: return@collect @@ -1999,10 +1767,7 @@ class DetailsViewModel( _state.value.repository?.id?.let { id -> } } - // Run the existing install dialog flow on the - // downloaded file. This is the unchanged - // validation + fingerprint + installer + DB save - // path that the VM has always owned. + try { installAsset( isUpdate = isUpdate, @@ -2012,9 +1777,7 @@ class DetailsViewModel( sizeBytes = sizeBytes, releaseTag = releaseTag, ) - // Successful install — release the entry - // from the orchestrator so the apps row - // doesn't keep showing "ready to install". + downloadOrchestrator.dismiss(packageKey) } catch (e: kotlinx.coroutines.CancellationException) { throw e @@ -2162,9 +1925,7 @@ class DetailsViewModel( when (validationResult) { is ApkValidationResult.ExtractionFailed -> { - // Don't block installation — proceed without - // validation (same as the Shizuku path). - // PackageEventReceiver will sync the DB post-install. + logger.warn( "Could not extract APK info for $assetName, " + "proceeding with unvalidated install", @@ -2231,9 +1992,6 @@ class DetailsViewModel( } } - // Serialize Android system installer dialogs only — desktop - // installers don't fire the broadcast that releases the gate, so - // gating there would block the next install for the full timeout. val gatePackageName = if (platform == Platform.ANDROID) validatedApkInfo?.packageName else null if (gatePackageName != null) { @@ -2249,7 +2007,6 @@ class DetailsViewModel( throw e } - // Launch attestation check asynchronously (non-blocking) launchAttestationCheck(filePath) if (platform == Platform.ANDROID && validatedApkInfo != null) { @@ -2317,8 +2074,7 @@ class DetailsViewModel( ) { val repo = _state.value.repository ?: return val isPending = installOutcome != InstallOutcome.COMPLETED - // Only carry the parked path through when the row is actually - // pending — a completed install must not store a stale pointer. + val pendingPath = parkedFilePath?.takeIf { isPending } if (isUpdate) { @@ -2331,9 +2087,7 @@ class DetailsViewModel( isPendingInstall = isPending, ), ) - // For pending updates, also park the file path on the row - // so the apps list can resume the install in one tap if - // the user dismissed the system prompt. + if (pendingPath != null) { runCatching { installedAppsRepository.setPendingInstallFilePath( @@ -2347,10 +2101,7 @@ class DetailsViewModel( } } } else { - // Snapshot the installable list as the user saw it at install - // time — this is the reference the variant fingerprint is - // relative to (pinning "the same kind of APK" means the same - // choice among these specific siblings). + val installable = _state.value.installableAssets val pickedIndex = installable .indexOfFirst { it.name == assetName } @@ -2375,14 +2126,6 @@ class DetailsViewModel( } } - /** - * "Download only" entry point — used by the action that - * downloads an asset without auto-installing it (e.g. for users - * who want to side-load via a different installer). Routes - * through the orchestrator with [InstallPolicy.DeferUntilUserAction] - * so the file is parked at AwaitingInstall and the user can pick - * it up from the apps row whenever they're ready. - */ private fun downloadAsset( downloadUrl: String, assetName: String, @@ -2392,7 +2135,7 @@ class DetailsViewModel( currentDownloadJob?.cancel() val packageKey = orchestratorKey() val repository = _state.value.repository ?: return - // Use the exact asset the user tapped, not the auto-picked primary. + val asset = _state.value.selectedRelease?.assets ?.find { it.downloadUrl == downloadUrl } ?: _state.value.primaryAsset @@ -2496,20 +2239,9 @@ class DetailsViewModel( override fun onCleared() { super.onCleared() - // Cancel the orchestrator OBSERVER (not the orchestrator - // entry itself). The download keeps running in the - // application-scoped orchestrator scope. + currentDownloadJob?.cancel() - // Tell the orchestrator that the foreground watcher is gone: - // any in-flight download with policy InstallWhileForeground - // should switch to DeferUntilUserAction so the file gets - // parked + the user gets a notification when bytes are done. - // Race-safe — the orchestrator handles "already past park - // time" by retroactively notifying. - // - // NonCancellable so the call runs to completion even though - // viewModelScope is being torn down around us. val packageKey = orchestratorKey() viewModelScope.launch(NonCancellable) { try { @@ -2535,10 +2267,7 @@ class DetailsViewModel( val repo = when { - // Forgejo / Codeberg path — repoId is a synthetic - // foreign id (see RepoIdCodec), so the GitHub - // `/repositories/{id}` endpoint can't resolve it. - // Owner+name must be supplied by the caller. + sourceHostParam != null -> { if (ownerParam.isBlank() || repoParam.isBlank()) { error("Foreign-source Details opened without owner/repo for host=$sourceHostParam") @@ -2559,10 +2288,6 @@ class DetailsViewModel( } launch { seenReposRepository.markAsSeen(repo) } - // Launch both checks in parallel before awaiting either — - // previously `isFavoriteDeferred.await()` ran before - // `isStarredDeferred` was even started, serialising two - // independent reads on the Details screen cold path. val isFavoriteDeferred = async { try { @@ -2653,10 +2378,7 @@ class DetailsViewModel( val userProfileDeferred = async { - // GitHub user profile lookup is GitHub-only — skip - // for Forgejo / Codeberg repos to avoid hitting the - // wrong API + leaking a 404 toast. Author card just - // hides when null. + if (sourceHostParam != null) return@async null try { detailsRepository.getUserProfile(owner) @@ -2674,7 +2396,6 @@ class DetailsViewModel( try { val dbApps = installedAppsRepository.getAppsByRepoId(repo.id) - // Reconcile pending-install status for each tracked app dbApps.map { dbApp -> if (dbApp.isPendingInstall && packageMonitor.isPackageInstalled(dbApp.packageName) @@ -2712,12 +2433,7 @@ class DetailsViewModel( ) if (rateLimited.get()) { - // Any deferred tripping the rate-limit flag leaves the UI - // in an incomplete state. Flag the releases section as - // failed so it renders its FAILED card with a Retry - // affordance instead of the misleading EMPTY card ("no - // releases published yet") — the default would be EMPTY - // because allReleases stays at its initial empty list. + _state.value = _state.value.copy( isLoading = false, errorMessage = null, @@ -2770,7 +2486,6 @@ class DetailsViewModel( insights.latestStableHasInstallableAsset, ) - observeInstalledApp(repo.id) maybeAutoTranslate( @@ -2832,9 +2547,7 @@ class DetailsViewModel( _state.update { it.copy(isRefreshing = true) } viewModelScope.launch { try { - // Refresh is GitHub-backend-only. For Forgejo repos there's - // no backend mediator; we just re-fetch via the Forgejo APIs - // directly using the same load path. + val refreshed = if (sourceHostParam != null) { detailsRepository.getRepositoryByOwnerAndName( owner = owner, @@ -2966,9 +2679,7 @@ class DetailsViewModel( tweaksRepository.getAutoTranslateEnabled().first() }.getOrDefault(false) if (!enabled) return@launch - // Treat blank explicit target as "unset" so fallback chain can - // run — `explicit = ""` would otherwise short-circuit the - // `?:` operator and disable auto-translate. + val explicit = runCatching { tweaksRepository.getAutoTranslateTargetLang().first() }.getOrNull()?.takeIf { it.isNotBlank() } @@ -2991,12 +2702,7 @@ class DetailsViewModel( getCurrentState = { _state.value.aboutTranslation }, ) } - // Source-language guard mirrors the README branch — if the - // release-notes source language already matches the target, - // skip the translation round-trip. The release model carries - // no language hint, so we fall back to `readmeLanguage` as the - // best available signal for repositories that consistently - // author release notes in the repo's primary language. + val releaseSourceLang = currentReadmeLang if (!releaseDescription.isNullOrBlank() && _state.value.whatsNewTranslation.translatedText == null && diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt index 80bd0e7b6..b7e6860cf 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt @@ -293,17 +293,6 @@ private fun PermissionsSection(permissions: List) { return@InspectSection } - // Per-group expand/collapse state lives in the sheet itself — - // not worth a VM round-trip. NORMAL / UNKNOWN buckets start - // collapsed because they're the long, low-signal lists; the - // spicy DANGEROUS / PRIVILEGED / SIGNATURE groups are open. - // - // Keyed on the permissions reference so opening the sheet for a - // different APK rebuilds the map with that APK's defaults - // (otherwise a previous app's "expanded NORMAL" choice would - // leak forward — fine right now because the sheet is - // recreated per visibility toggle, but cheap insurance against - // a future change that keeps the sheet alive across inspections). val expanded = remember(permissions) { mutableStateMapOf().apply { @@ -435,9 +424,7 @@ private fun PermissionRow(permission: ApkPermission) { color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - // Demoted to a footnote — small, lower-opacity, monospace. - // The technical name is useful for search and bug reports - // but it shouldn't compete with the human-readable label. + Text( text = permission.name, style = MaterialTheme.typography.labelSmall, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt index ae2c367e1..67cf82d60 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt @@ -97,11 +97,7 @@ fun AppHeader( modifier = Modifier.size(100.dp), ) { CoilImage( - // Fall back to `repository.owner.avatarUrl` when no - // user profile was fetched — covers the Forgejo / - // Codeberg path where we skip the GitHub-only - // `/users/{login}` lookup, as well as transient - // profile fetch failures on the GitHub path. + imageModel = { author?.avatarUrl ?: repository.owner.avatarUrl }, modifier = Modifier diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt index 1e99e1f87..5c774562a 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt @@ -48,15 +48,6 @@ import zed.rainxch.githubstore.core.presentation.res.apk_inspect_coachmark_body import zed.rainxch.githubstore.core.presentation.res.apk_inspect_coachmark_dismiss import zed.rainxch.githubstore.core.presentation.res.apk_inspect_coachmark_title -/** - * Discoverable entry point for the APK Inspect sheet. - * - * Renders a 52dp circular icon button next to the install button. When - * [showCoachmark] is true, the button does a slow pulse + tilt and a - * tooltip-style coachmark renders above the icon, anchored with an - * arrow. The coachmark is one-shot per user — tapping it (or the - * button) dismisses and persists via [onCoachmarkDismiss]. - */ @Composable fun InspectApkButton( showCoachmark: Boolean, @@ -122,7 +113,7 @@ private fun rememberTilt(active: Boolean) = private fun Coachmark(onDismiss: () -> Unit) { Popup( alignment = Alignment.TopEnd, - // Render above the button. Negative Y offset moves the popup up. + offset = androidx.compose.ui.unit.IntOffset(x = 0, y = -260), properties = PopupProperties( focusable = false, @@ -178,7 +169,7 @@ private fun Coachmark(onDismiss: () -> Unit) { } } } - // Triangle arrow pointing down at the icon button. + Box( modifier = Modifier .padding(end = 24.dp) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt index 01e067779..d5386723f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt @@ -106,7 +106,6 @@ fun LanguagePicker( .padding(horizontal = 16.dp, vertical = 8.dp), ) - // Device language shortcut — only shown when not searching if (searchQuery.isBlank() && deviceLanguage != null) { Row( modifier = diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt index 962c57d34..d35fc69aa 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt @@ -76,10 +76,7 @@ fun ReleaseAssetsPicker( showAllPlatforms: Boolean = false, crossPlatformAssets: List = emptyList(), ) { - // Decouple from `showAllPlatforms`: the toggle lives INSIDE the sheet, - // so disabling the open-card whenever the current branch is empty - // would lock the user out of flipping the setting back. Picker stays - // openable whenever either source has anything to show. + val isPickerEnabled by remember(assetsList, crossPlatformAssets) { derivedStateOf { assetsList.isNotEmpty() || crossPlatformAssets.isNotEmpty() @@ -198,10 +195,6 @@ private fun ReleaseAssetsItemsPicker( } } - // "Pinned to: … [Unpin]" hint, only when the user actually - // has a pin. Surfaces both the current pin and a one-tap - // unpin affordance — the only place in the app where a pin - // can be removed without picking a different one. if (!pinnedVariant.isNullOrBlank()) { Row( modifier = @@ -222,10 +215,6 @@ private fun ReleaseAssetsItemsPicker( } } - // Cross-platform toggle. Persisted globally — flipping here - // changes every Details screen's picker behaviour for this - // user. Off = current-OS assets only; On = grouped sections - // for Android / Windows / macOS / Linux. Surface( shape = RoundedCornerShape(20.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh, @@ -260,9 +249,6 @@ private fun ReleaseAssetsItemsPicker( } } - // Hoisted out of the LazyListScope below: `LazyColumn { … }` - // body is not a @Composable context, so `remember` calls have - // to live in the enclosing Column instead. val groups = remember(crossPlatformAssets) { crossPlatformAssets .groupBy { @@ -279,16 +265,9 @@ private fun ReleaseAssetsItemsPicker( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - // Grouped path only when the toggle is on AND the release - // has platform-classifiable assets. If toggle is on but - // `assetPlatformOf` rejected every asset (e.g. release - // ships only .zip bundles / extensionless binaries), we - // fall through to the OFF-mode `assetsList` render so - // the user still sees the current-platform installables - // instead of an empty sheet. + if (showAllPlatforms && groups.isNotEmpty()) { - // Order: current-platform section first (it's the - // primary install target), then the others. + val sectionOrder = listOf( zed.rainxch.core.domain.model.DiscoveryPlatform.Android to Res.string.platform_section_android, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt index e411a70e2..b8a887785 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt @@ -88,24 +88,11 @@ fun SmartInstallButton( val isUpdateAvailable = installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall - // VersionMath.isExactSameVersion gates the CTA: it requires both - // sides to be present and compares post-prefix-strip strings literally, - // so build-metadata variants (`1.0.0+build.1` vs `1.0.0+build.2`) are - // correctly treated as different versions even though semver-style - // ordering would consider them equal. Reuse the trimmed selected tag - // downstream so the "Install version X" label can never render empty. val normInstalled = installedApp?.installedVersion?.trim()?.takeIf { it.isNotBlank() } val normSelected = state.selectedRelease?.tagName?.trim()?.takeIf { it.isNotBlank() } - // Some maintainers tag releases with path-style strings such as - // `com.akylas.documentscanner/android/github/1.21.0/152`. Using the raw - // tag in "Install version X" wraps the CTA across two lines and looks - // broken. `VersionMath.normalizeVersion` already extracts the - // dotted-digit core for these cases (`1.21.0`), so reuse it for the - // *display* tag while leaving the actual install pipeline on the raw - // tag (which has to match GitHub exactly). val displaySelected = normSelected?.let { tag -> VersionMath.normalizeVersion(tag).takeIf { it.isNotBlank() } ?: tag @@ -124,14 +111,13 @@ fun SmartInstallButton( val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE - // When same version is installed, show Open button if (isSameVersionInstalled && !isActiveDownload) { Column(modifier = modifier) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - // Uninstall button + ElevatedCard( onClick = { onAction(DetailsAction.OnRequestUninstall) }, modifier = @@ -174,7 +160,6 @@ fun SmartInstallButton( } } - // Open button ElevatedCard( modifier = Modifier @@ -225,7 +210,6 @@ fun SmartInstallButton( return } - // Regular install/update button for all other cases val buttonColor = when { !enabled && !isActiveDownload -> MaterialTheme.colorScheme.surfaceContainer @@ -240,11 +224,6 @@ fun SmartInstallButton( stringResource(Res.string.not_available) } - // Highest priority: a previously-deferred download is - // already on disk and matches the current selection. - // Tell the user they can install in one tap, no - // re-download. The actual short-circuit lives in - // DetailsViewModel.installAsset. state.isPendingInstallReady -> { stringResource(Res.string.install_ready) } @@ -263,13 +242,6 @@ fun SmartInstallButton( stringResource(Res.string.install_version, displaySelected ?: normSelected) } - // Not installed yet, but the user reached for the release picker - // and chose something other than the newest available release — - // the CTA should reflect *which* version will install instead of - // misleading them with "Install latest". The latest tag comes - // from the head of `allReleases` (GitHub returns newest-first - // by `published_at`); fall through to "Install latest" only when - // we genuinely can't tell, or the user is already on the head. normSelected != null && state.allReleases.firstOrNull()?.tagName?.let { latestTag -> !VersionMath.isExactSameVersion(latestTag, normSelected) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt index 68b675c95..5b46ceb8b 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt @@ -223,12 +223,7 @@ private fun VersionListItem( } } if (release.isEffectivelyPreRelease()) { - // Prefer the specific marker ("Beta", "RC", "Alpha"…) - // over the generic "Pre-release" pill — a stronger - // signal for users deciding whether to install. Falls - // back to the generic badge only when the API flag - // marks a release as pre-release but no recognised - // marker is in the tag or name. + val specificLabel = release.preReleaseLabel() Surface( shape = RoundedCornerShape(4.dp), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt index 81b4dd565..f5265809e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt @@ -161,23 +161,10 @@ fun ExpandableMarkdownContent( val colors = rememberMarkdownColors() val typography = rememberMarkdownTypography() - // Pre-process markdown off the main thread. The theme-aware image rewrite - // is regex-heavy; running it inside `remember { ... }` happened on the - // composition thread (typically Main) and contributed to the visible - // freeze on first render and theme toggle. We launch it on Default and - // gate rendering on completion via `fullChunks` being non-null. - // - // We split the full body into ~4 000-char chunks (kept whole around - // code fences). Chunk 0 doubles as the collapsed preview; subsequent - // chunks stream in one frame at a time once the user taps Expand. - // Crucially, chunk 0 stays mounted across collapse → expand, so the - // transition is a height grow rather than a content swap — no flicker. var fullChunks by remember(rawMarkdown, isDark) { mutableStateOf?>(null) } LaunchedEffect(rawMarkdown, isDark) { val processed = withContext(Dispatchers.Default) { - // Theme-aware image substitution first, then split adjacent - // image-link rows into their own paragraphs so badge stacks - // render as block-level images (no inline-overlap). + val themed = applyThemeAwareImages(rawMarkdown, isDark) zed.rainxch.core.domain.util.separateAdjacentImageLinks(themed) } @@ -187,9 +174,6 @@ fun ExpandableMarkdownContent( fullChunks = chunks } - // Parser + flavour are heavy to construct and identical across recompositions; - // hoist them once so they survive content / theme / scroll churn. The Markdown - // lib parses lazily on Dispatchers.Default once handed a fresh MarkdownState. val flavour = remember { GFMFlavourDescriptor() } val parser = remember(flavour) { MarkdownParser(flavour) } val components = remember(isDark, imageTransformer) { @@ -281,21 +265,6 @@ fun ExpandableMarkdownContent( } } -/** - * Renders chunked markdown progressively. Chunk 0 is always mounted - * (used as the collapsed-state preview, clipped by the parent's - * `Modifier.height(collapsedHeight)` modifier). When `isExpanded` flips - * true, subsequent chunks stream in one frame at a time without - * unmounting / remounting chunk 0 — that stable identity is what kills - * the previous expand-time flicker. - * - * Each chunk is its own `Markdown(...)` composable with its own - * `MarkdownState` (parser runs on `Dispatchers.Default` per the - * mikepenz lib). The "one per frame" cadence keeps composition cost - * predictable instead of dropping the entire body's compose pass on - * Main in a single 4-second hit (observed Davey on a kubernetes-sized - * README before this change). - */ @Composable internal fun ProgressiveMarkdown( isExpanded: Boolean, @@ -325,15 +294,11 @@ internal fun ProgressiveMarkdown( return } - // Counter starts at 1 so chunk 0 (the natural preview) renders - // immediately for the collapsed view. Once the user expands, the - // remaining chunks stream in one frame at a time. var renderedCount by remember(rawKey) { mutableStateOf(1) } LaunchedEffect(rawKey, isExpanded, chunks.size) { if (!isExpanded) return@LaunchedEffect while (renderedCount < chunks.size) { - // Yield to the frame so the previously-added chunk has a chance - // to compose + layout + draw before the next chunk arrives. + kotlinx.coroutines.yield() renderedCount++ } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt index 2bf876977..a74230eee 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt @@ -80,14 +80,6 @@ fun LazyListScope.header( } } - // Status card replaces the pickers + install button in three cases: - // 1. releases fetch failed — show error + Retry - // 2. retry in flight — show spinner - // 3. repo truly has no releases — show "no releases published" empty state - // Initial page load (isLoading) is intentionally excluded — the top-level - // loading spinner covers it, no need to double up. Same for repository - // not loaded yet: the release-specific states only make sense once we - // know the repo exists (matches the VM's retryReleases() guard). val releasesStatus: ReleasesStatus? = when { state.repository == null -> null @@ -106,7 +98,7 @@ fun LazyListScope.header( ) } } else { - // versions type list + if (state.allReleases.isNotEmpty()) { item { VersionTypePicker( @@ -117,7 +109,6 @@ fun LazyListScope.header( } } - // version and installable release if (state.allReleases.isNotEmpty() || state.installableAssets.isNotEmpty()) { item { Row( @@ -125,9 +116,7 @@ fun LazyListScope.header( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { - // Memoize the platform-classifiable subset of release - // assets so each Header recompose (scroll, animation, - // unrelated state change) doesn't re-run the filter. + val crossPlatformAssets = androidx.compose.runtime.remember(state.selectedRelease) { state.selectedRelease @@ -138,10 +127,7 @@ fun LazyListScope.header( } .orEmpty() } - // Refresh pinned label from the currently matched asset - // — stored value may carry a stale qualifier-counter - // prefix (e.g. `beta.24-arm64-v8a`) from when the pin - // was first set. Issue #612. + val pinnedVariantLabel = state.installedApp?.preferredAssetVariant?.let { stored -> state.primaryAsset?.name?.let { name -> @@ -171,16 +157,9 @@ fun LazyListScope.header( } item { - // Inspect button only surfaces once the package is genuinely - // installed on device (`isReallyInstalled()` filters out - // pending-install rows whose `installedApp` is non-null but - // the system hasn't confirmed the install). This avoids - // popping the icon in at the exact frame the system install - // prompt appears, which is the user's peak-attention moment. + val canInspectApk = state.installedApp?.isReallyInstalled() == true - // Even when visible, the coachmark animation only fires - // during a calm moment — never while a download or install - // is mid-flight, never with the inspect sheet already open. + val coachmarkActive = state.isApkInspectCoachmarkPending && canInspectApk && diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt index 1acc74ae6..57079c9d5 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt @@ -29,11 +29,6 @@ fun LazyListScope.logs(state: DetailsState) { ) } - // `timeIso` is second-precision, so the same APK installed twice in - // one second would collide on a (timeIso + assetName) composite key. - // Including the list index makes the key unconditionally unique while - // still cheaper than no-key (kept stable across recompositions of the - // same list shape). itemsIndexed( items = state.installLogs, key = { index, log -> "$index|${log.timeIso}|${log.assetName}" }, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt index 0a116801c..e88d571e0 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt @@ -57,20 +57,6 @@ import zed.rainxch.githubstore.core.presentation.res.stalled_project_warning_day import zed.rainxch.githubstore.core.presentation.res.stalled_project_warning_description import zed.rainxch.githubstore.core.presentation.res.stalled_project_warning_months -/** - * Release-channel UX bundle for the Details screen - * (GitHub-Store release UX #2, #3, #4, #6): - * - Inline chip to toggle per-app pre-release channel. - * - "Switch to stable vX.Y.Z" chip when user is on a pre-release - * and a stable is available. - * - Stalled-project warning card when the latest stable is old - * but pre-releases are still flowing. - * - Merged "What's changed since v…" card that concatenates - * release notes across every version the user has skipped. - * - * All four are additive — nothing renders when the app isn't - * tracked or when the corresponding signal is absent. - */ fun LazyListScope.releaseChannel( state: DetailsState, onAction: (DetailsAction) -> Unit, @@ -107,9 +93,7 @@ fun LazyListScope.releaseChannel( ChannelChip( label = channelLabel, icon = Icons.Default.Science, - // Visually signal the "hot" channel when the user - // has opted into betas; keep it muted when they're - // on the default stable-only track. + tint = if (includeBetas) { MaterialTheme.colorScheme.tertiary @@ -117,12 +101,7 @@ fun LazyListScope.releaseChannel( MaterialTheme.colorScheme.onSurfaceVariant }, onClick = { onAction(DetailsAction.ToggleIncludeBetas) }, - // Mirror the visible label so screen readers hear - // the current channel ("Include betas" / "Stable - // only") instead of the previous static - // "Toggle beta releases for this app" string, - // which gave no indication of which side the - // toggle is currently on. + contentDescriptionText = channelLabel, ) if (state.isChannelChipCoachmarkPending) { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt index ffc4646a3..ec63cfa04 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt @@ -163,7 +163,6 @@ private fun ExpandableMarkdownContent( } val isDark = androidx.compose.foundation.isSystemInDarkTheme() - // Off-main pre-processing — see About.kt for the rationale. var fullChunks by remember(raw, isDark) { mutableStateOf?>(null) } LaunchedEffect(raw, isDark) { val processed = withContext(Dispatchers.Default) { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/AlertBlockQuote.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/AlertBlockQuote.kt index e33f2da63..8610697ea 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/AlertBlockQuote.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/AlertBlockQuote.kt @@ -43,7 +43,7 @@ enum class GithubAlertKind(val token: String) { ; companion object { - // [!KIND] on its own line, allowing trailing spaces. + private val PATTERN = Regex("""^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)]\s*$""") fun parse(blockquoteText: String): Match? { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/ExpandableDetails.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/ExpandableDetails.kt index c474569e1..51b2771f0 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/ExpandableDetails.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/ExpandableDetails.kt @@ -129,12 +129,7 @@ private fun decodeDetailsSummary(encoded: String): String { } private fun extractFenceBody(model: MarkdownComponentModel): String { - // Slice the original source between the first and last - // CODE_FENCE_CONTENT child. Iterating child-by-child and - // re-inserting newlines double-counts (prepend on content + EOL - // token), producing a blank line between every body line — which - // breaks GFM tables, lists, and any block that needs contiguous - // source lines. + val content = model.content val contentNodes = model.node.children.filter { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT } if (contentNodes.isEmpty()) return "" diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/GithubStoreMarkdownComponents.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/GithubStoreMarkdownComponents.kt index 5925f31f3..f1e01c61b 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/GithubStoreMarkdownComponents.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/GithubStoreMarkdownComponents.kt @@ -14,9 +14,6 @@ import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.MarkdownTokenTypes import org.intellij.markdown.ast.ASTNode -// Plain (non-@Composable) factory so callers can wrap in `remember(isDark)` -// — the wrapped MarkdownComponents holds lambdas that get invoked inside the -// Markdown render scope, but constructing the wrapper has no composition cost. fun githubStoreMarkdownComponents( imageTransformer: ImageTransformer, isDark: Boolean, @@ -47,19 +44,6 @@ fun githubStoreMarkdownComponents( image = { model -> LinkAwareMarkdownImage(model.content, model.node, imageTransformer) }, ) -/** - * Drop-in replacement for the lib's default `MarkdownImage` that also - * supports the `[![alt](image-url)](href)` pattern — i.e. the image is a - * clickable hyperlink. README badges (Maven Central status, build status, - * sponsor buttons, "Get it on Play Store" / "Get it on F-Droid" tiles) - * all use that pattern. - * - * The lib's stock renderer pulls the image's own `LINK_DESTINATION` (the - * src) but ignores the outer INLINE_LINK that wraps it. We walk up via - * `ASTNode.parent`; if an ancestor is an INLINE_LINK, we grab its - * `LINK_DESTINATION` and apply `Modifier.clickable { openUri }` around - * the rendered image. - */ @Composable private fun LinkAwareMarkdownImage( content: String, @@ -72,13 +56,6 @@ private fun LinkAwareMarkdownImage( val outerHref = findEnclosingLinkDestination(node, content) val imageData = imageTransformer.transform(imageSrc) ?: return - // Block-level images: GitHub-style sizing. Cap width to the - // content column, let height flow naturally from the intrinsic - // aspect ratio. No height cap — a tall screenshot renders at its - // full proportional height and the user scrolls past it, matching - // what github.com / `.markdown-body img { max-width: 100% }` does. - // Inline rendering still uses the lib's `Placeholder` slot via - // `MarkdownImageTransformer`. val blockModifier = androidx.compose.ui.Modifier.fillMaxWidth() if (outerHref != null) { @@ -118,14 +95,11 @@ private fun findChildRecursive(node: ASTNode, type: IElementType): ASTNode? { private fun findEnclosingLinkDestination(imageNode: ASTNode, content: String): String? { var cursor: ASTNode? = imageNode.parent - // Walk at most a few levels — INLINE_LINK is usually one or two - // parents up. Bail before drifting into the surrounding paragraph. + var depth = 0 while (cursor != null && depth < 4) { if (cursor.type == MarkdownElementTypes.INLINE_LINK) { - // The INLINE_LINK's direct LINK_DESTINATION child is the href. - // Use the non-recursive child lookup so we don't accidentally - // pick up the image's own (inner) LINK_DESTINATION. + val destNode = cursor.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION } ?: return null diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/SyntaxHighlightedCode.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/SyntaxHighlightedCode.kt index 7fbd3baf9..86f8b1cae 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/SyntaxHighlightedCode.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/markdown/SyntaxHighlightedCode.kt @@ -44,18 +44,12 @@ fun SyntaxHighlightedCode( extractFenceContent(model.node, model.content) } - // Highlight tokenization is CPU-heavy on big code blocks. Run it on - // Default and render the plain code immediately so the markdown pass - // never blocks waiting for highlighting — Main-thread ANR observed - // previously with multiple large fences on a single README. var highlighted by remember(code, language, isDark) { mutableStateOf(AnnotatedString(code)) } LaunchedEffect(code, language, isDark) { if (code.isEmpty() || language == SyntaxLanguage.DEFAULT) return@LaunchedEffect - // Skip giant code blocks (e.g. embedded JSON dumps, generated YAML). - // The Highlights tokenizer is super-linear in input size and the - // payoff for plain-eye reading shrinks fast past a few thousand chars. + if (code.length > MAX_HIGHLIGHTABLE_CHARS) return@LaunchedEffect val result = withContext(Dispatchers.Default) { buildHighlighted(code, language, isDark) @@ -81,9 +75,7 @@ fun SyntaxHighlightedCode( } private fun extractFenceContent(node: ASTNode, content: String): Pair { - // Code fence node children: - // ``` opener, lang token (FENCE_LANG), eol, CODE_FENCE_CONTENT lines, - // ``` closer. Walk children to extract language hint + raw code body. + var language: SyntaxLanguage = SyntaxLanguage.DEFAULT val body = StringBuilder() var sawContent = false diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt index c595fe116..ed92afeb5 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt @@ -33,23 +33,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -/** - * Skips rendering of images whose advertised `Content-Length` exceeds - * [MAX_IMAGE_BYTES]. A HEAD probe runs once per URL on `Dispatchers.IO` - * via the injected [probeClient] (re-used: the same client used for - * proxy testing in `core.data.di.SharedModule` under the `test` - * qualifier — short request timeouts, no extra interceptors). Results - * are cached process-wide so a README that references the same big - * image twice still probes once. - * - * Decoded-bitmap dimension is also capped at [MAX_BITMAP_DIMENSION_PX] - * so a 4K source image (Content-Length under the byte threshold) is - * still rescaled before reaching the GPU. - * - * Servers that omit `Content-Length` (chunked transfer, mis-configured - * CDNs) are treated as "size unknown" and rendered normally — the byte - * cap is best-effort, not a guarantee. - */ class MarkdownImageTransformer( private val probeClient: HttpClient, ) : ImageTransformer { @@ -96,10 +79,6 @@ class MarkdownImageTransformer( probeResult = result } - // Skip rendering entirely when the advertised payload would have - // burned bandwidth + decode time. Returning `null` makes the - // markdown renderer omit the image element; alt text (if any) - // remains visible in the surrounding paragraph. if (probeResult is ProbeResult.Skipped) return null val context = LocalPlatformContext.current @@ -119,18 +98,12 @@ class MarkdownImageTransformer( val isBadgeLike = looksLikeBadge(normalizedLink) val inlineModifier = if (isBadgeLike) { - // SVG / shields.io / GitHub Actions badge / Codecov tile — - // narrow vector content. Cap tight so several badges tile - // on one line without overlap. + Modifier .heightIn(max = BADGE_MAX_HEIGHT_DP.dp) .widthIn(max = BADGE_MAX_WIDTH_DP.dp) } else { - // Raster image (PNG / JPG / WEBP / GIF) — typically a - // banner, hero, or screenshot. Give it room while still - // bounded so a stray oversized PNG can't take over the - // page. Block-level rendering of these goes through - // `LinkAwareMarkdownImage` and ignores this modifier. + Modifier .heightIn(max = RASTER_MAX_HEIGHT_DP.dp) .widthIn(max = RASTER_MAX_WIDTH_DP.dp) @@ -146,20 +119,16 @@ class MarkdownImageTransformer( private fun looksLikeBadge(url: String): Boolean { val lower = url.lowercase() - // 1. Explicit `.svg` (with or without query string). Covers most - // repos' own action badges + custom shields. + val pathOnly = lower.substringBefore('?').substringBefore('#') if (pathOnly.endsWith(".svg")) return true - // 2. Known badge providers — many serve SVG without an - // extension in the URL (`https://img.shields.io/badge/...`). + val host = lower .removePrefix("https://") .removePrefix("http://") .substringBefore('/') return host in BADGE_HOSTS || - // 3. Path-based hints — GitHub Actions workflow badges - // (`/actions/workflows/.../badge.svg`), Open Source - // Insights, Bestpractices.dev tiles. + "/badge" in pathOnly || "/badges/" in pathOnly || "/workflows/" in pathOnly && pathOnly.endsWith("/badge") || @@ -169,31 +138,6 @@ class MarkdownImageTransformer( @Composable override fun intrinsicSize(painter: Painter): Size = painter.intrinsicSize - /** - * Override the lib's per-paragraph placeholder size so badge rows - * (the typical `[![…](svg)](url) [![…](svg)](url)` stack in a - * README header) don't all inherit the FIRST image's placeholder - * dimensions — which is what produces the observed overlap when the - * first image is an SVG with no intrinsic size (`Size(0, 180)` - * defaults from the lib) and every following badge takes a 180sp - * tall slot in the same line. - * - * Strategy: when the painter has not yet reported an intrinsic - * size, allocate a badge-sized slot (32sp tall, 120sp wide) instead - * of the lib's 180×180 default. That tiles cleanly horizontally on - * a single line. Once Coil decodes and the painter reports a real - * `intrinsicImageSize`, we scale into the container width with the - * height capped at [RASTER_MAX_HEIGHT_DP] (the larger of the two - * per-kind ceilings, so a single shared placeholder per paragraph - * still fits a raster image without vertical overflow); when that - * cap trims height we scale width proportionally so aspect ratio - * and placeholder slot stay consistent. Badge images remain bounded - * at 40 dp by their own modifier inside this slot. Container width of `0f` (first - * composition before the layout pass) is treated the same as - * unspecified — otherwise the placeholder collapses to zero size, - * the image is invisible on first frame, and a second - * recomposition is forced once layout reports a real width. - */ override fun placeholderConfig( density: androidx.compose.ui.unit.Density, containerSize: Size, @@ -205,12 +149,7 @@ class MarkdownImageTransformer( intrinsicImageSize.height <= 0f -> BADGE_DEFAULT_WIDTH_SP to BADGE_DEFAULT_HEIGHT_SP else -> with(density) { - // Treat a zero container width the same as unspecified. - // Compose can hand us `containerSize.width == 0f` on the - // first composition before the surrounding paragraph - // has been measured; without this fallback the - // placeholder collapses to zero size and the image - // disappears for one frame. + val containerWidthPx = when { containerSize.isUnspecified -> intrinsicImageSize.width containerSize.width <= 0f -> intrinsicImageSize.width @@ -220,24 +159,10 @@ class MarkdownImageTransformer( val ratio = intrinsicImageSize.height / intrinsicImageSize.width.coerceAtLeast(1f) val rawHeight = initialWidth * ratio - // Cap at the LARGER of the two per-kind modifier - // ceilings (raster). The mikepenz lib uses a single - // shared `Placeholder` per paragraph, so if a paragraph - // contains a genuinely inline raster image (e.g. - // `Some text ![screenshot](img.png) more text`) and we - // capped here at `BADGE_MAX_HEIGHT_DP` (40 dp), the - // placeholder slot would be 40 dp while the raster's - // own modifier still allows 320 dp — the image would - // overflow the text flow vertically. Using the raster - // cap reserves enough slot for either kind; badge - // images are still independently bounded at 40 dp by - // their own modifier inside this slot. + val maxHeightPx = RASTER_MAX_HEIGHT_DP.dp.toPx() val targetHeight = rawHeight.coerceAtMost(maxHeightPx) - // Preserve aspect ratio: when height was clamped, scale - // the width down by the same factor so the placeholder - // box matches the actual rendered image shape — avoids - // excess horizontal whitespace and badge re-wrapping. + val targetWidth = if (rawHeight > maxHeightPx && ratio > 0f) { targetHeight / ratio } else { @@ -264,9 +189,7 @@ class MarkdownImageTransformer( else -> ProbeResult.Allowed } }.getOrElse { - // Probe failed (timeout, CORS, network off). Allow render; - // Coil itself will surface the error if the real request - // fails. We don't penalise unreachable servers here. + ProbeResult.Allowed } probeCache[url] = result @@ -285,38 +208,19 @@ class MarkdownImageTransformer( "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/126.0.0.0 Mobile Safari/537.36 GitHubStore/1.8" - // Hard cap on the decoded bitmap dimension (see class kdoc). const val MAX_BITMAP_DIMENSION_PX = 2048 - // Hard cap on the advertised payload size. Above this we skip the - // image entirely. 5 MB covers ~98% of repo READMEs in the wild; - // anything above is almost always a generated mega-screenshot or - // an uncompressed source export that would be unreadable inline - // anyway. const val MAX_IMAGE_BYTES = 5L * 1024 * 1024 - // Default inline slot for badges (most common SVG inline case). - // 32sp tall fits a Shields.io / GitHub Actions badge with room - // to breathe; 120sp wide is the typical "Get it on …" width. private const val BADGE_DEFAULT_WIDTH_SP = 120f private const val BADGE_DEFAULT_HEIGHT_SP = 32f - // SVG / vector badge bounds — designed for narrow vector - // content. Single source of truth: placeholder cap and - // ImageData.modifier height cap derive from the same dp - // constant so the placeholder slot and the rendered Image - // remain in lock-step at every density. private const val BADGE_MAX_HEIGHT_DP = 40 private const val BADGE_MAX_WIDTH_DP = 220 - // Raster image bounds — banners, screenshots, hero shots. Looser - // height + width so they're actually legible inline (when they - // appear inline at all — most are top-level via LinkAwareMarkdownImage). private const val RASTER_MAX_HEIGHT_DP = 320 private const val RASTER_MAX_WIDTH_DP = 480 - // Hosts that overwhelmingly serve SVG badges, often without a - // `.svg` URL suffix. Matched case-insensitively. private val BADGE_HOSTS = setOf( "img.shields.io", "shields.io", @@ -333,9 +237,6 @@ class MarkdownImageTransformer( ) .build() - // Process-wide probe cache. README rerenders, theme toggles, and - // recompositions all share the same map so the HEAD round-trip - // only happens once per URL per app session. private val probeCache = mutableMapOf() private val probeMutex = Mutex() } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownTruncate.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownTruncate.kt index 2d6f69024..68dd6555a 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownTruncate.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownTruncate.kt @@ -1,19 +1,9 @@ package zed.rainxch.details.presentation.utils -/** - * Returns a markdown substring suitable for rendering as a collapsed - * "preview" without the cost of composing the full tree. Truncates at a - * sensible boundary (double newline, then single newline, then `maxChars`) - * so headings and code fences aren't sliced mid-block. - * - * Always callable from any thread — no Compose APIs touched. - */ fun truncateMarkdownPreview(content: String, maxChars: Int): String { if (content.length <= maxChars) return content val window = content.substring(0, maxChars) - // Prefer cutting at paragraph break (blank line) inside the last 25% - // of the window so the preview ends on a natural section boundary. val searchFrom = (maxChars * 0.75).toInt().coerceAtLeast(0) val paragraphBreak = window.lastIndexOf("\n\n", maxChars).takeIf { it >= searchFrom } if (paragraphBreak != null && paragraphBreak > 0) { @@ -26,21 +16,6 @@ fun truncateMarkdownPreview(content: String, maxChars: Int): String { return window.trimEnd() + "…" } -/** - * Splits a markdown document into roughly equal chunks at top-level block - * boundaries (double newlines), each ≤ `targetChunkChars`. Each chunk is a - * self-contained snippet the markdown parser can handle in isolation; we - * render them as separate `Markdown(...)` composables so the expensive - * AST-to-Compose pass happens once per chunk rather than once for the - * entire document. - * - * Code fences are kept whole (we never slice between ```...```), even if - * that makes one chunk larger than `targetChunkChars`. Without that - * guard the parser would emit half-open fence nodes and the renderer - * would print raw backticks. - * - * Always callable from any thread — no Compose APIs touched. - */ fun splitMarkdownIntoChunks(content: String, targetChunkChars: Int): List { if (content.length <= targetChunkChars) return listOf(content) val chunks = mutableListOf() @@ -55,9 +30,7 @@ fun splitMarkdownIntoChunks(content: String, targetChunkChars: Int): List AssistChip( - onClick = { /* No action */ }, + onClick = { }, label = { Text( text = language, @@ -194,7 +194,7 @@ fun FavouriteRepositoryItem( favouriteRepository.latestRelease?.let { release -> AssistChip( - onClick = { /* No action */ }, + onClick = { }, label = { Text( text = release, @@ -214,7 +214,7 @@ fun FavouriteRepositoryItem( } AssistChip( - onClick = { /* No action */ }, + onClick = { }, label = { Text( text = favouriteRepository.addedAtFormatter, diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt index 3148bfdff..066ba61ae 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt @@ -83,7 +83,6 @@ class CachedRepositoriesDataSourceImpl( } } - // Try backend first val backendResult = fetchTopicFromBackend(topic, platform) if (backendResult != null) { cacheMutex.withLock { @@ -93,7 +92,6 @@ class CachedRepositoriesDataSourceImpl( return backendResult } - // Fallback to raw GitHub JSON logger.debug("Backend failed for topic $topicCacheKey, falling back to GitHub raw JSON") return fetchTopicFromFallback(topic, platform, topicCacheKey) } @@ -115,7 +113,6 @@ class CachedRepositoriesDataSourceImpl( } } - // Try backend first val backendResult = fetchCategoryFromBackend(category, platform) if (backendResult != null) { cacheMutex.withLock { @@ -125,13 +122,10 @@ class CachedRepositoriesDataSourceImpl( return backendResult } - // Fallback to raw GitHub JSON logger.debug("Backend failed for $cacheKey, falling back to GitHub raw JSON") return fetchCategoryFromFallback(category, platform, cacheKey) } - // ── Backend fetchers ────────────────────────────────────────────── - private suspend fun fetchCategoryFromBackend( category: HomeCategory, platform: DiscoveryPlatform, @@ -185,7 +179,6 @@ class CachedRepositoriesDataSourceImpl( }.awaitAll().filterNotNull() } - // Only use backend result if all 4 platforms succeeded (mirrors fallback behavior) if (responses.isEmpty() || responses.size < platforms.size) return@withContext null val merged = responses @@ -267,7 +260,6 @@ class CachedRepositoriesDataSourceImpl( }.awaitAll().filterNotNull() } - // Only use backend result if all 4 platforms succeeded (mirrors fallback behavior) if (responses.isEmpty() || responses.size < platforms.size) return@withContext null val merged = responses @@ -296,8 +288,6 @@ class CachedRepositoriesDataSourceImpl( ) } - // ── Fallback fetchers (existing raw GitHub JSON) ────────────────── - private suspend fun fetchCategoryFromFallback( category: HomeCategory, platform: DiscoveryPlatform, @@ -482,8 +472,6 @@ class CachedRepositoriesDataSourceImpl( } } - // ── Helpers ─────────────────────────────────────────────────────── - private fun DiscoveryPlatform.toApiSlug(): String? = when (this) { DiscoveryPlatform.Android -> "android" DiscoveryPlatform.Windows -> "windows" diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt index 1040fb7ad..0e8afda3c 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt @@ -70,9 +70,6 @@ class HomeRepositoryImpl( return "home:$category:$token:page$page" } - // Mirror cache only ships per-platform snapshots and a merged "all" file. - // For 2-3 platform selections we pull the merged set and filter by - // intersection on the client side rather than fetching N snapshots. private suspend fun loadCachedReposForSet( platforms: Set, fetchSingle: suspend (DiscoveryPlatform) -> CachedRepoResponse?, @@ -547,8 +544,7 @@ class HomeRepositoryImpl( return when { topics.isEmpty() -> baseQuery topics.size == 1 -> "$baseQuery topic:${topics.first()}" - // Classic REST search doesn't support parenthesized qualifier grouping. - // Repeat the full base query per topic joined with OR. + else -> topics.joinToString(separator = " OR ") { "($baseQuery topic:$it)" } } } @@ -562,7 +558,6 @@ class HomeRepositoryImpl( DiscoveryPlatform.Linux -> "linux" } - /** Treat `All` and a fully-covering set as "no filter" (empty set). */ private fun Set.normalize(): Set { if (contains(DiscoveryPlatform.All)) return emptySet() val real = filter { it != DiscoveryPlatform.All }.toSet() diff --git a/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt b/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt index 199485dd7..69e95efd8 100644 --- a/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt +++ b/feature/home/domain/src/commonMain/kotlin/zed/rainxch/home/domain/repository/HomeRepository.kt @@ -32,11 +32,5 @@ interface HomeRepository { platforms: Set, ): Flow - /** - * Fetch a single repo summary by id. Used by the Home starred section to - * hydrate stale local cache rows with fresh topics / updatedAt / platforms. - * Returns `null` on failure (network / 404) so callers can drop that one - * item without failing the whole section. - */ suspend fun getRepositoryById(id: Long): zed.rainxch.core.domain.model.GithubRepoSummary? } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt index 72efcb7c0..22f84080f 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt @@ -15,18 +15,6 @@ internal fun daysSinceIso(isoInstant: String): Int { return (diffMs / 86_400_000L).toInt() } -/** - * Sub-day-aware relative label. Falls back to "Nd" once gap ≥ 24h. Inputs: - * - <1m → "now" - * - <1h → "Nm" - * - <24h → "Nh" - * - <30d → "Nd" - * - <12mo → "Nmo" - * - else → "Ny" - * - * Used by the Hot card corner pill + Lead card "HOT · X ago" so a fresh release - * 4 hours old shows "4h" instead of "0d". - */ @OptIn(ExperimentalTime::class) internal fun relativeAgo(isoInstant: String): String { val trimmed = isoInstant.trim() diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt index e40f539c0..39f5c1486 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -133,7 +133,7 @@ fun HotCardItem( } } } - // Suppress unused dark-mode for now; reserved for future accent-bloom tweak. + @Suppress("UNUSED_EXPRESSION") isDark } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index c664a0db8..661d81be7 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -114,39 +114,39 @@ class ProfileViewModel( } ProfileAction.OnLoginClick -> { - // Handed in composable + } ProfileAction.OnFavouriteReposClick -> { - // Handed in composable + } ProfileAction.OnStarredReposClick -> { - // Handed in composable + } is ProfileAction.OnRepositoriesClick -> { - // Handed in composable + } ProfileAction.OnRecentlyViewedClick -> { - // Handed in composable + } ProfileAction.OnWhatsNewClick -> { - // Handed in composable + } ProfileAction.OnWhatsNewLongClick -> { - // Handed in composable + } ProfileAction.OnAnnouncementsClick -> { - // Handed in composable + } ProfileAction.OnAnnouncementsLongClick -> { - // Handed in composable + } } } diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt index 323ba903a..cddeba343 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt @@ -98,7 +98,6 @@ class SearchRepositoryImpl( return@channelFlow } - // Try backend search first val backendResult = tryBackendSearch(query, platform, sortBy, page) if (backendResult != null) { cacheManager.put(cacheKey, backendResult, SEARCH_RESULTS) @@ -106,7 +105,6 @@ class SearchRepositoryImpl( return@channelFlow } - // Fallback to GitHub REST search fallbackGithubSearch(query, platform, language, sortBy, sortOrder, page, cacheKey) }.flowOn(Dispatchers.IO) @@ -153,8 +151,6 @@ class SearchRepositoryImpl( ) } - // ── Backend search ──────────────────────────────────────────────── - private suspend fun tryBackendSearch( query: String, platform: DiscoveryPlatform, @@ -163,9 +159,6 @@ class SearchRepositoryImpl( ): PaginatedDiscoveryRepositories? { if (query.isBlank()) return null - // Backend doesn't support forks sorting — fall through to GitHub - // REST. RecentlyUpdated and RecentlyReleased route through backend - // (sort=updated / sort=releases respectively). if (sortBy == SortBy.MostForks) return null val platformSlug = when (platform) { @@ -181,7 +174,7 @@ class SearchRepositoryImpl( SortBy.BestMatch -> "relevance" SortBy.RecentlyUpdated -> "updated" SortBy.RecentlyReleased -> "releases" - SortBy.MostForks -> null // unreachable, guarded above + SortBy.MostForks -> null } val offset = (page - 1) * BACKEND_PAGE_SIZE @@ -206,20 +199,7 @@ class SearchRepositoryImpl( ) }, onFailure = { e -> - // Centralized fallback policy. Side effects: - // * 429 → throws domain RateLimitException so the - // caller surfaces a friendly retry-after toast - // (prevents the direct-GitHub /search + per-repo - // /releases verify storm that would otherwise burn - // the user's quota and trip the global rate-limit - // dialog). - // * CancellationException → re-thrown to preserve - // structured concurrency. - // * BackendException 5xx / network → returns true → - // fall through to GitHub REST fallback below. - // * BackendException 4xx (other than 429) → returns - // false → backend's answer is authoritative; do - // NOT silently retry against direct GitHub. + if (!shouldFallbackToGithubOrRethrow(e)) { throw e } @@ -228,8 +208,6 @@ class SearchRepositoryImpl( ) } - // ── Fallback GitHub REST search ─────────────────────────────────── - private suspend fun kotlinx.coroutines.channels.ProducerScope.fallbackGithubSearch( query: String, platform: DiscoveryPlatform, diff --git a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt index f3373a6b4..a474c0b54 100644 --- a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt @@ -8,13 +8,6 @@ enum class SortBy { RecentlyReleased, ; - /** - * GitHub's REST `/search/repositories?sort=...` doesn't expose a - * "by latest release date" axis — only repo-level activity. When the - * backend isn't reachable and we fall through here, RecentlyReleased - * borrows `updated` semantics (closest available approximation) so - * the UI doesn't degrade silently to relevance order. - */ fun toGithubSortParam(): String? = when (this) { MostStars -> "stars" diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index bd492e35a..f8cb19ff1 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -229,11 +229,6 @@ fun SearchScreen( } } - // Auto-paginate while every loaded repo is filtered out by the global - // "Hide seen" tweak. The grid is empty → scroll-based shouldLoadMore can - // never fire (totalItemsCount == 0), so without this the user is stranded - // on a blank screen with no way to reach page 2+ where unseen repos may - // live. Stops once hasMorePages flips false; the banner then takes over. LaunchedEffect( state.repositories.size, state.visibleRepos.size, @@ -330,7 +325,7 @@ fun SearchScreen( .padding(innerPadding) .padding(horizontal = 16.dp), ) { - // Clipboard banner + AnimatedVisibility( visible = state.isClipboardBannerVisible && state.clipboardLinks.isNotEmpty(), enter = slideInVertically() + fadeIn(), @@ -347,7 +342,6 @@ fun SearchScreen( ) } - // Detected links from search query AnimatedVisibility( visible = state.detectedLinks.isNotEmpty(), enter = slideInVertically() + fadeIn(), @@ -527,7 +521,6 @@ fun SearchScreen( ) } - // Show search history when query is empty if (state.query.isBlank() && state.repositories.isEmpty() && state.recentSearches.isNotEmpty() && @@ -593,10 +586,6 @@ fun SearchScreen( Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = stringResource(Res.string.no_repositories_found)) - // Backend already did its own passthrough and still found - // nothing — don't tease a manual explore that would just - // redo the same work. Any other case (false / null for - // older backends) keeps the CTA. if (state.passthroughAttempted != true) { Spacer(Modifier.height(8.dp)) ExploreFromGithubButton( @@ -608,12 +597,6 @@ fun SearchScreen( } } - // Auto-paginate is fetching the next page in the background - // because every loaded repo is filtered out. Without an - // explicit indicator the content area is blank: the top-level - // spinner only renders while repositories.isEmpty(), and the - // in-grid load-more spinner is unreachable because the grid - // itself only renders when visibleRepos is non-empty. if (state.repositories.isNotEmpty() && state.visibleRepos.isEmpty() && state.isHideSeenEnabled && @@ -637,16 +620,6 @@ fun SearchScreen( } } - // All currently loaded API hits filtered out by the global - // "Hide seen" tweak AND no further pages remain — results - // counter still shows the raw total, so without this banner - // the user sees "N results found" above an empty grid and - // assumes the app is broken (issue #574). `hasMorePages` is - // required because the scroll-based pagination above stalls - // when totalItemsCount == 0, so we surface the banner only - // when there is genuinely nothing more to fetch. While - // hasMorePages is true, the auto-paginate effect below pulls - // the next page in the background. if (state.repositories.isNotEmpty() && state.visibleRepos.isEmpty() && state.isHideSeenEnabled && @@ -685,9 +658,7 @@ fun SearchScreen( columns = StaggeredGridCells.Adaptive(350.dp), verticalItemSpacing = 12.dp, horizontalArrangement = Arrangement.spacedBy(12.dp), - // Bottom clearance = nav pill + FAB (~56dp standard M3 - // FAB, positioned at bottomNavHeight + 16.dp) + breathing - // room so the last card scrolls fully above both. + contentPadding = PaddingValues( start = 8.dp, @@ -745,7 +716,6 @@ fun SearchScreen( } } - // "Fetch more from GitHub" explore button if (!state.isLoading && !state.isLoadingMore && state.query.isNotBlank()) { item { ExploreFromGithubButton( diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index e576b66bf..3183d5cbe 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -533,7 +533,7 @@ class SearchViewModel( it.copy(selectedLanguage = action.language) } currentPage = 1 - + performSearch(isInitial = true) } } @@ -616,7 +616,7 @@ class SearchViewModel( it.copy(selectedSortBy = action.sortBy) } currentPage = 1 - + performSearch(isInitial = true) } } @@ -627,7 +627,7 @@ class SearchViewModel( it.copy(selectedSortOrder = action.sortOrder) } currentPage = 1 - + performSearch(isInitial = true) } } diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt index bb4ec5962..0bf027b23 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt @@ -171,10 +171,7 @@ class StarredReposViewModel( } StarredReposAction.OnRefresh -> { - // Refresh may return a list that no longer matches the active - // search query, leaving the user staring at an empty grid with - // a stale filter still applied. Clearing the query removes the - // ambiguity. + _state.update { it.copy(searchQuery = "") } syncStarredRepos(forceRefresh = true) } diff --git a/feature/tweaks/presentation/src/androidMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.android.kt b/feature/tweaks/presentation/src/androidMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.android.kt index 32b3b03ad..c0d151dbd 100644 --- a/feature/tweaks/presentation/src/androidMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.android.kt +++ b/feature/tweaks/presentation/src/androidMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.android.kt @@ -1,13 +1,5 @@ package zed.rainxch.tweaks.presentation -/** - * No-op on Android — runtime language changes are applied via - * `Activity.recreate()` from `MainActivity`, and the triggering - * event (`OnAppLanguageChangeRequiresRestart`) is never emitted on - * this platform. We keep an actual so common code compiles; if the - * invariant ever breaks we'd rather silently skip than kill the - * process. - */ actual fun restartAppAfterLanguageChange() { - // Intentionally empty. + } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.kt index 885fb28f0..4f68e683f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.kt @@ -1,19 +1,3 @@ package zed.rainxch.tweaks.presentation -/** - * Platform hook for the "restart now" Snackbar action after a - * language change on Desktop. Tries to spawn a fresh JVM with the - * same command line as the current process and then exit — so the - * user ends up in a freshly-started app with their new locale - * applied by `DesktopApp.main`. If that isn't possible (IDE/Gradle - * runs, sandbox restrictions, etc.) the implementation falls back - * to a plain exit; the user's preference is already persisted, so - * they just need to reopen the app manually. - * - * This should never be invoked on Android — `MainActivity` handles - * runtime language changes via `Activity.recreate()`, and the - * `OnAppLanguageChangeRequiresRestart` event that triggers this is - * never emitted there. The Android actual is therefore a no-op for - * safety. - */ expect fun restartAppAfterLanguageChange() diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index b5cb3dbcc..f9c351aba 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -194,10 +194,6 @@ sealed interface TweaksAction { val tag: String?, ) : TweaksAction - /** - * User picked a UI language. `tag == null` means "follow system - * locale" — cleared persisted preference. - */ data class OnAppLanguageSelected( val tag: String?, ) : TweaksAction @@ -206,12 +202,6 @@ sealed interface TweaksAction { data object OnDismissBatteryOptimizationCard : TweaksAction - /** - * Re-evaluates whether the battery-optimization card should still - * show. Fired on Tweaks resume so the card disappears immediately - * after the user grants the exemption from the system Settings - * screen. - */ data object OnReevaluateBatteryOptimizationCard : TweaksAction data object OnOpenCustomForgesDialog : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt index d3556e074..55e5177c9 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt @@ -33,13 +33,5 @@ sealed interface TweaksEvent { data object OnMicrosoftTranslatorCredentialsSaved : TweaksEvent - /** - * Fired on platforms where changing the UI language cannot be - * applied in-place (currently Desktop — no `Activity.recreate()` - * equivalent). The UI prompts the user to restart so the new - * locale takes effect on the next cold start. On Android this - * event is never emitted; `MainActivity` handles runtime changes - * via `recreate()` directly. - */ data object OnAppLanguageChangeRequiresRestart : TweaksEvent } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index f7712c3c6..4575620ee 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -60,8 +60,7 @@ fun TweaksRoot( androidx.lifecycle.LifecycleEventObserver { _, event -> if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) { viewModel.onAction(TweaksAction.OnRefreshCacheSize) - // Re-eval after returning from system battery-optimization - // settings so the card disappears immediately on grant. + viewModel.onAction(TweaksAction.OnReevaluateBatteryOptimizationCard) } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 572a03174..7e08fedf3 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -39,15 +39,7 @@ data class TweaksState( val isScrollbarEnabled: Boolean = false, val contentWidth: ContentWidth = ContentWidth.COMPACT, val translationProvider: TranslationProvider = TranslationProvider.Default, - /** - * Transient UI-only selection used when the user picks a provider - * that needs more configuration before it can be activated (e.g. - * Youdao with missing credentials). Rendered as the "selected - * chip" when non-null; persisted [translationProvider] is the - * source of truth for what the app actually uses for translation. - * Cleared once the pending selection is either committed - * (credentials saved) or abandoned (another provider picked). - */ + val draftTranslationProvider: TranslationProvider? = null, val youdaoAppKey: String = "", val youdaoAppSecret: String = "", @@ -60,38 +52,22 @@ data class TweaksState( val microsoftTranslatorKey: String = "", val microsoftTranslatorRegion: String = "", val isMicrosoftTranslatorKeyVisible: Boolean = false, - /** - * User-selected UI language as a BCP 47 tag, or `null` to follow - * the system locale. Mirrors the preference observed by - * `MainViewModel` — surfaced here so the Tweaks picker can show - * which chip is selected. - */ + val selectedAppLanguage: String? = null, val autoTranslateEnabled: Boolean = false, val autoTranslateTargetLang: String? = null, val isFeedbackSheetVisible: Boolean = false, - /** - * True only on aggressive-OEM Android devices (Oppo, OnePlus, Realme, - * Xiaomi, vivo, Honor) that have NOT yet whitelisted the app from - * battery optimization AND the user has not dismissed the prompt. - * Drives the "Allow background updates" card in the Updates section. - */ + val showBatteryOptimizationCard: Boolean = false, val customForgeHosts: Set = emptySet(), val showCustomForgesDialog: Boolean = false, val customForgeDraft: String = "", val customForgeError: String? = null, ) { - /** Effective provider to render as "selected" in the UI — draft - * overrides persisted when a pending selection is in flight. */ + val displayedTranslationProvider: TranslationProvider get() = draftTranslationProvider ?: translationProvider - /** Convenience accessor — returns a fresh default if the map is - * missing an entry for [scope]. The constructor seeds all scopes, - * but `copy(proxyForms = …)` call sites could in theory produce an - * incomplete map; the safe default keeps the UI from crashing in - * that case. */ fun formFor(scope: ProxyScope): ProxyScopeFormState = proxyForms[scope] ?: ProxyScopeFormState() } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 3538ba80c..401cdfaea 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -60,22 +60,14 @@ class TweaksViewModel( private companion object { private const val BATTERY_OPT_PREF_READ_TIMEOUT_MS: Long = 1_000 - // IPv4 dotted-quad, each octet 0..255. private val IPV4_PATTERN = Regex( "^(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)" + "(\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)){3}$", ) - // IPv6 literal — character-level only (hex groups + colons, - // optional `::` shortcut). Permissive on canonical form because - // proxy clients normalize it; we only need to reject "looks - // wrong" inputs like `not a url`. private val IPV6_PATTERN = Regex("^[0-9A-Fa-f:]+$") - // RFC 1123 hostname: labels of 1..63 alphanumeric / hyphen, - // must start and end with alphanumeric, separated by dots. - // Total length capped at 253 by the caller. private val HOSTNAME_PATTERN = Regex( "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)" + @@ -114,10 +106,7 @@ class TweaksViewModel( hasLoadedInitialData = true } refreshCacheSize() - // Re-evaluate on every Tweaks (re-)entry: the user may - // have whitelisted the app from system Settings since - // the last evaluation, in which case the card should - // disappear without requiring an explicit dismiss. + evaluateBatteryOptimizationCard() }.stateIn( scope = viewModelScope, @@ -215,17 +204,7 @@ class TweaksViewModel( } private fun loadProxyConfig() { - // Start one collector per scope. Each updates its slot in the - // [TweaksState.proxyForms] map — scopes are independent so the - // flows intentionally don't share state. - // - // If the user has an in-progress edit on a scope (isDraftDirty) - // we skip hydration for that scope until they commit (save) or - // reset (switch type via OnProxyTypeSelected for None/System). - // DataStore emits on *any* preference change — without this - // guard, toggling any unrelated setting while the user is mid- - // typing in the host field would snap the form back to persisted - // values. + ProxyScope.entries.forEach { scope -> viewModelScope.launch { proxyRepository.getProxyConfig(scope).collect { config -> @@ -269,8 +248,6 @@ class TweaksViewModel( } } - /** User-triggered form edit — marks the scope dirty so the - * preferences flow won't clobber the edit on an unrelated emit. */ private fun mutateForm( scope: ProxyScope, block: (ProxyScopeFormState) -> ProxyScopeFormState, @@ -283,11 +260,6 @@ class TweaksViewModel( } } - /** Transient UI-state mutation (password visibility, test-in- - * progress) — does *not* mark the scope dirty, so toggling the eye - * icon or running a test doesn't block preference hydration. Only - * use for flags that don't represent a real config change the user - * expects to save. */ private fun mutateFormUi( scope: ProxyScope, block: (ProxyScopeFormState) -> ProxyScopeFormState, @@ -299,9 +271,6 @@ class TweaksViewModel( } } - /** Clears the dirty flag — call after a successful save or an - * explicit reset so the next preferences emission can re-hydrate - * the form. */ private fun clearDirty(scope: ProxyScope) { _state.update { state -> val form = state.formFor(scope) @@ -604,10 +573,7 @@ class TweaksViewModel( is TweaksAction.OnProxyTypeSelected -> { mutateForm(action.scope) { it.copy(type = action.type) } - // NONE / SYSTEM have no form fields — persist immediately - // since there's nothing left for the user to fill in. For - // HTTP / SOCKS, wait for an explicit Save so validation - // can run against a completed form. + if (action.type == ProxyType.NONE || action.type == ProxyType.SYSTEM) { val config = if (action.type == ProxyType.NONE) { @@ -619,8 +585,7 @@ class TweaksViewModel( runCatching { proxyRepository.setProxyConfig(action.scope, config) }.onSuccess { - // Committed — allow preferences-flow hydration - // to resume for this scope. + clearDirty(action.scope) _events.send(TweaksEvent.OnProxySaved) }.onFailure { error -> @@ -658,11 +623,7 @@ class TweaksViewModel( is TweaksAction.OnProxySave -> { val form = _state.value.formFor(action.scope) - // Only HTTP/SOCKS need host+port — validate for those - // only. NONE/SYSTEM carry no form fields and would - // otherwise be rejected with "host required" for no - // reason if something ever triggered Save for them - // (today the UI doesn't, but defense in depth). + val config: ProxyConfig = when (form.type) { ProxyType.NONE -> ProxyConfig.None @@ -733,7 +694,7 @@ class TweaksViewModel( try { proxyTester.test(config) } catch (e: CancellationException) { - // Preserve structured concurrency — never swallow. + throw e } catch (e: Exception) { ProxyTestOutcome.Failure.Unknown(e.message) @@ -909,8 +870,7 @@ class TweaksViewModel( is TweaksAction.OnTranslationProviderSelected -> { when (action.provider) { TranslationProvider.GOOGLE -> { - // No credentials required — persist immediately - // and clear any pending draft selection. + _state.update { it.copy(draftTranslationProvider = null) } viewModelScope.launch { tweaksRepository.setTranslationProvider(action.provider) @@ -929,24 +889,14 @@ class TweaksViewModel( _events.send(TweaksEvent.OnTranslationProviderSaved) } } else { - // No credentials yet — expose the selection as - // a draft so the UI expands the credentials - // form, but don't commit to storage. If we - // persisted here the next translation attempt - // would fail with "not configured" and any - // other repository that observes the flow - // would snap back on the next re-emission. - // Committed later from [OnYoudaoCredentialsSave]. + _state.update { it.copy(draftTranslationProvider = TranslationProvider.YOUDAO) } } } TranslationProvider.LIBRE_TRANSLATE -> { - // No gating — repository falls back to the - // public Disroot mirror when no URL configured, - // so first-tap "just works" without going - // through a config dialog. + _state.update { it.copy(draftTranslationProvider = null) } viewModelScope.launch { tweaksRepository.setTranslationProvider(action.provider) @@ -1005,12 +955,7 @@ class TweaksViewModel( viewModelScope.launch { tweaksRepository.setYoudaoAppKey(current.youdaoAppKey) tweaksRepository.setYoudaoAppSecret(current.youdaoAppSecret) - // Auto-switch to YOUDAO when the user explicitly saves - // credentials — saves them an extra tap and matches - // the implicit intent ("I just configured this, use - // it"). Also covers the "draft" case where the chip - // was picked but not yet persisted because creds - // were missing. + val shouldActivate = current.youdaoAppKey.isNotBlank() && current.youdaoAppSecret.isNotBlank() && @@ -1021,8 +966,7 @@ class TweaksViewModel( if (shouldActivate) { tweaksRepository.setTranslationProvider(TranslationProvider.YOUDAO) } - // Drop any draft — either we committed it above or - // the user emptied fields and cancelled implicitly. + _state.update { it.copy(draftTranslationProvider = null) } _events.send(TweaksEvent.OnYoudaoCredentialsSaved) } @@ -1205,9 +1149,7 @@ class TweaksViewModel( if (result.isSuccess) { _state.update { it.copy(customForgeDraft = "", customForgeError = null) } } else { - // Surface persistence failure (KSafe write - // rejected by hardware-backed Keystore, etc.) - // instead of silently clearing the draft. + _state.update { it.copy( customForgeError = result.exceptionOrNull()?.message @@ -1234,12 +1176,6 @@ class TweaksViewModel( } } - /** - * Builds the [ProxyConfig] to test from the current form state for [scope]. - * For [ProxyType.HTTP] / [ProxyType.SOCKS] this requires a valid host and - * port — if either is missing the user is told via an error event and - * `null` is returned, mirroring the validation in [TweaksAction.OnProxySave]. - */ private fun buildProxyConfigForTest(scope: ProxyScope): ProxyConfig? { val form = _state.value.formFor(scope) return when (form.type) { @@ -1286,19 +1222,6 @@ class TweaksViewModel( } } - /** - * Light-but-real proxy host validator. Accepts: - * - IPv4 dotted-quad (`192.168.1.1`) with octet range 0..255 - * - IPv6 literal, with or without surrounding brackets (`::1`, - * `[2001:db8::1]`) — character-level only, doesn't validate - * canonical form - * - RFC 1123 hostname — labels of 1..63 alphanumeric / hyphen - * characters separated by dots, must start/end with alphanumeric - * - * Rejects: garbage strings (`not a url`), schemes (`http://...`), - * paths (`example.com/api`), spaces, control characters. Mirrors the - * port validator's "reject and show clear error" contract. - */ private fun isValidProxyHost(raw: String): Boolean { val host = raw.trim() if (host.isBlank()) return false @@ -1338,10 +1261,7 @@ class TweaksViewModel( ) is ProxyTestOutcome.Failure.Unknown -> - // Raw exception messages are platform-specific, untranslated, - // and may leak internal detail — always show the localized - // fallback to the user. The original `message` is intentionally - // dropped here; surface it via logging if diagnostics are needed. + TweaksEvent.OnProxyTestError(getString(Res.string.proxy_test_error_unknown)) } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt index 9575a8105..f3dd0d34d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt @@ -38,9 +38,7 @@ fun CustomForgesDialog( title = { Text(stringResource(Res.string.custom_forges_dialog_title)) }, text = { Column { - // Reassure the user: Codeberg ships built-in, so adding - // it here is a no-op. The dialog is purely for users - // running their own Forgejo / Gitea host. + Surface( color = MaterialTheme.colorScheme.tertiaryContainer, shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index b7bfdeb16..405fc614f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -95,7 +95,6 @@ fun LazyListScope.installationSection( }, ) - // Auto-update toggle — shown when a silent installer is selected and ready val silentReady = ( state.installerType == InstallerType.SHIZUKU && state.shizukuAvailability == ShizukuAvailability.READY @@ -284,11 +283,6 @@ private fun AttributionRadioRow( } } -/** - * Updates section — always visible on Android (not gated on Shizuku). - * Shows the update check interval picker so all users can configure - * how often background update checks run. - */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.updatesSection( state: TweaksState, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt index 785edb813..494a0b054 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt @@ -116,11 +116,7 @@ internal fun LanguageDropdown( } Box(modifier = Modifier.fillMaxWidth()) { - // Anchor row — tappable area that shows the current value and - // toggles the menu. Uses a `surface`-tinted background so it - // reads as a pickable control against the parent card's - // `surfaceContainer`; the plain clickable row otherwise - // blends into the card. + Row( modifier = Modifier @@ -154,15 +150,10 @@ internal fun LanguageDropdown( expanded = expanded, onDismissRequest = { expanded = false }, shape = RoundedCornerShape(20.dp), - // Default menu container is `surfaceContainer`, the same - // tone the parent `ElevatedCard` uses — the menu would - // visually dissolve into the card. Step up to - // `surfaceContainerHigh` so it reads as a distinct popup - // layer with the correct elevation contrast. + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) { - // Follow-system first — it's the default and users - // escaping a wrong-language lock-in look for this first. + DropdownMenuItem( text = { DropdownItemText(stringResource(Res.string.language_follow_system)) }, onClick = { @@ -183,9 +174,7 @@ internal fun LanguageDropdown( AppLanguages.ALL.forEach { language -> DropdownMenuItem( text = { - // Native-script label so a user stuck in the - // wrong language can still recognise their - // own and escape. + DropdownItemText(language.displayName) }, onClick = { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt index a6f4f70c0..c9ed13c4e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt @@ -158,8 +158,6 @@ fun LazyListScope.networkSection( Spacer(Modifier.height(16.dp)) } - // One card per scope. Ordering mirrors the user's mental model: - // browsing → downloading → translation (least-common last). ProxyScope.entries.forEach { scope -> item { ProxyScopeCard( @@ -299,9 +297,7 @@ private fun ProxyDetailsFields( portValue.isNotEmpty() && (portValue.toIntOrNull()?.let { it !in 1..65535 } ?: true) val hostValue = form.host - // Real-time host validation mirrors the port pattern: only flag as - // invalid once the user has typed something, so the empty initial - // state doesn't shout an error at them. + val isHostInvalid = hostValue.isNotEmpty() && !isLikelyValidProxyHost(hostValue) val isFormValid = hostValue.isNotEmpty() && @@ -406,12 +402,7 @@ private fun ProxyDetailsFields( ) { ProxyTestButton( isInProgress = form.isTestInProgress, - // Keep enabled regardless of form validity so the user - // never taps a disabled button by accident on the first - // press (the previous `isFormValid` gate raced with - // the IME-composition-commit step on some Android - // keyboards and silently swallowed taps). VM still - // validates and surfaces a clear error event. + enabled = !form.isTestInProgress, onClick = { keyboardController?.hide() @@ -422,12 +413,7 @@ private fun ProxyDetailsFields( FilledTonalButton( onClick = { - // Hide IME first so any pending composition commits - // synchronously before the VM reads form state. - // `clearFocus()` alone isn't enough on Gboard / - // SwiftKey, which keep characters in a composition - // buffer until focus actually transfers — that's - // what made the user have to tap Save twice. + keyboardController?.hide() focusManager.clearFocus() onAction(TweaksAction.OnProxySave(scope)) @@ -478,13 +464,6 @@ private fun ProxyTestButton( } } -/** - * UI-side mirror of `TweaksViewModel.isValidProxyHost`. Drives the - * real-time `isError` highlight on the host TextField so the user - * sees a problem before they tap Save — same UX as the port field. - * Authoritative validation still happens server-side in the VM; this - * is the optimistic, free-of-cost preview. - */ private fun isLikelyValidProxyHost(raw: String): Boolean { val host = raw.trim() if (host.isBlank()) return false diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt index 4b1001f79..75d68ec94 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt @@ -279,16 +279,6 @@ private fun providerLabel(provider: TranslationProvider): String = TranslationProvider.MICROSOFT -> stringResource(Res.string.translation_provider_microsoft) } -/** - * Dropdown for picking the auto-translate target language. - * - * Distinct from [LanguageDropdown]: that one is bound to `AppLanguages` — - * the 13 locales the app ships UI translations for. Translation targets - * are a wider set ([SupportedTranslationLanguages.all] — 33 entries - * spanning everything the translation service can produce, including - * German, Dutch, Portuguese, Ukrainian, Vietnamese, etc. that the app - * itself isn't translated into). - */ @Composable private fun TranslationTargetDropdown( selectedTag: String?, @@ -460,8 +450,7 @@ private fun LibreTranslateCredentialsForm( state: TweaksState, onAction: (TweaksAction) -> Unit, ) { - // Always allow save — empty URL is a valid state (falls back to - // the bundled public mirror in the repository layer). + val canSave = true Column( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackEvent.kt index 3f6ccc56e..4350d4215 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackEvent.kt @@ -3,9 +3,7 @@ package zed.rainxch.tweaks.presentation.feedback import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel sealed interface FeedbackEvent { - /** Emitted after `BrowserHelper.openUrl` returned without invoking - * `onFailure`. The host (TweaksRoot) collapses the sheet and - * shows a per-channel success snackbar. */ + data class OnSent(val channel: FeedbackChannel) : FeedbackEvent data class OnSendError(val message: String) : FeedbackEvent diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt index 948b59a68..9e1e859aa 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt @@ -83,9 +83,7 @@ class FeedbackViewModel( _events.send(FeedbackEvent.OnSendError(error)) } } - // Hold the disabled state briefly so the user sees the - // buttons disable and can't double-tap; long enough to - // also let any synchronous onFailure invocation arrive. + delay(250) _state.update { it.copy(isSending = false) } if (!failed) { @@ -96,8 +94,7 @@ class FeedbackViewModel( } private fun resetForm() { - // Preserve already-collected diagnostics so we don't re-query - // repositories when the sheet reopens. + _state.update { previous -> FeedbackState(diagnostics = previous.diagnostics) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt index 715c0c2c5..de49db7b3 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt @@ -67,7 +67,7 @@ fun ConditionalFields( onValueChange = { onAction(FeedbackAction.OnDesiredBehaviourChange(it)) }, ) } - FeedbackCategory.OTHER -> { /* no extras */ } + FeedbackCategory.OTHER -> { } } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt index 48e0b5de9..3a0283928 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt @@ -129,11 +129,6 @@ fun FeedbackBottomSheet( onAction = viewModel::onAction, ) - // Channel for the diagnostics preview is informational only — - // the actual channel is decided when the user picks Send. We - // pass GITHUB so the preview shows the username if present - // (most permissive view); the composer still strips it for - // the email send. DiagnosticsPreview( diagnostics = state.diagnostics, channel = FeedbackChannel.GITHUB, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt index 97010f1a7..b7b0c0144 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt @@ -38,7 +38,7 @@ object FeedbackComposer { builder.appendSection("Current behaviour", state.currentBehaviour) builder.appendSection("Desired behaviour", state.desiredBehaviour) } - FeedbackCategory.OTHER -> { /* no extra fields */ } + FeedbackCategory.OTHER -> { } } if (state.attachDiagnostics) { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt index f5473de06..cd41bedfd 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt @@ -51,8 +51,7 @@ class HiddenRepositoriesViewModel( } private fun unhide(repoId: Long) { - // Snapshot the row name BEFORE the flow re-emits without it, so the - // success snackbar can name what the user just acted on. + val fullName = _state.value.items.firstOrNull { it.repoId == repoId }?.fullName.orEmpty() viewModelScope.launch { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensAction.kt index 94be394ab..bbc9abdbe 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensAction.kt @@ -6,7 +6,6 @@ import zed.rainxch.core.domain.model.HostToken sealed interface HostTokensAction { data object OnNavigateBack : HostTokensAction - // Picker / dialog lifecycle data object OnAddClicked : HostTokensAction data object OnAddDismiss : HostTokensAction data class OnPickPresetForge(val kind: ForgeKind) : HostTokensAction @@ -15,13 +14,11 @@ sealed interface HostTokensAction { data class OnReplaceToken(val existing: HostToken) : HostTokensAction data class OnEditLabel(val existing: HostToken) : HostTokensAction - // Dialog field edits data class OnDraftHostChanged(val value: String) : HostTokensAction data class OnDraftTokenChanged(val value: String) : HostTokensAction data class OnDraftDisplayNameChanged(val value: String) : HostTokensAction data object OnAddConfirm : HostTokensAction - // Row-level actions data class OnValidate(val host: String) : HostTokensAction data class OnDelete(val host: String) : HostTokensAction data object OnUndoDelete : HostTokensAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEvent.kt index 9980ef452..5cee79f3a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEvent.kt @@ -5,9 +5,7 @@ import zed.rainxch.core.domain.model.HostToken sealed interface HostTokensEvent { data class Message(val text: String) : HostTokensEvent - /** Snackbar with undo when a token is removed. */ data class TokenDeletedWithUndo(val deleted: HostToken) : HostTokensEvent - /** Open URL in browser (token creation pages on each forge). */ data class OpenUrl(val url: String) : HostTokensEvent } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt index 34a57fd7b..cba2c700f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt @@ -231,11 +231,7 @@ fun HostTokensRoot( } private fun visiblePresetForges(state: HostTokensState): List { - // Hide the GitHub preset card when the user is already signed in via - // the in-app OAuth flow — for those users a PAT for github.com is - // redundant. Power users can still reach it via "Other forge". - // Always show forges the user already has a stored token for, so the - // picker stays consistent with the row list when those rows exist. + val storedHosts = state.tokens.map { it.host }.toSet() return ForgeKind.entries.filter { kind -> when { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensState.kt index 0f0f62c61..3043c1bf9 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensState.kt @@ -6,10 +6,9 @@ import zed.rainxch.core.domain.model.HostToken data class HostTokensState( val tokens: List = emptyList(), - // Inline validate feedback per row. Survives Snackbar dismiss so the - // user can re-check it on screen. + val validationByHost: Map = emptyMap(), - // Hosts whose validate request is in flight (row spinner). + val validatingHosts: Set = emptySet(), val draftMode: DraftMode = DraftMode.Closed, @@ -29,10 +28,8 @@ data class HostTokensState( sealed interface DraftMode { data object Closed : DraftMode - /** Forge picker step — shown on add tap or empty state. */ data object Picker : DraftMode - /** Token entry dialog. */ data class Compose(val replacingExisting: HostToken? = null) : DraftMode } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt index c00b24200..e1af1462a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensViewModel.kt @@ -37,10 +37,7 @@ class HostTokensViewModel( init { viewModelScope.launch { - // .catch so a downstream KSafe / serialization throw doesn't - // tear down the collector and leave the screen stuck on the - // initial loading spinner. We surface the failure once via - // Message and drop isLoading so the UI shows the empty state. + repository.observeAll() .catch { t -> _state.update { it.copy(isLoading = false) } @@ -61,7 +58,7 @@ class HostTokensViewModel( } viewModelScope.launch { userSessionRepository.isUserLoggedIn() - .catch { /* swallow — OAuth state is non-critical for this screen */ } + .catch { } .collect { signedIn -> _state.update { it.copy(isOAuthSignedInToGithub = signedIn) } } @@ -70,7 +67,7 @@ class HostTokensViewModel( fun onAction(action: HostTokensAction) { when (action) { - HostTokensAction.OnNavigateBack -> { /* host handles */ } + HostTokensAction.OnNavigateBack -> { } HostTokensAction.OnAddClicked -> _state.update { it.copy(draftMode = DraftMode.Picker) } @@ -259,10 +256,7 @@ class HostTokensViewModel( if (result.isSuccess) { _state.update { it.copy(pendingUndoDelete = null) } } else { - // Surface the failure but keep `pendingUndoDelete` populated so - // the user could trigger another undo (if surfaced) instead of - // losing the bytes silently. The actual "another undo" surface - // does not exist today; at minimum we emit the error. + _events.send( HostTokensEvent.Message( getString(Res.string.host_tokens_undo_failed, pending.host), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerViewModel.kt index eeed0d1e3..6456f5189 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerViewModel.kt @@ -51,7 +51,7 @@ class MirrorPickerViewModel( fun onAction(action: MirrorPickerAction) { when (action) { - MirrorPickerAction.OnNavigateBack -> { /* host handles via callback */ } + MirrorPickerAction.OnNavigateBack -> { } is MirrorPickerAction.OnSelectMirror -> selectMirror(action.mirror) MirrorPickerAction.OnCustomMirrorClicked -> _state.update { it.copy(isCustomDialogVisible = true, customDraft = "", customDraftError = null) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt index 1e76aed4b..01bdb8d63 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt @@ -1,12 +1,5 @@ package zed.rainxch.tweaks.presentation.model -/** - * Per-scope form backing state for the proxy section. Each - * [zed.rainxch.core.domain.model.ProxyScope] card owns one of these - * — keeps the in-progress, test, and visibility flags independent - * across scopes so the user can edit one card while a test runs on - * another. - */ data class ProxyScopeFormState( val type: ProxyType = ProxyType.SYSTEM, val host: String = "", @@ -15,13 +8,6 @@ data class ProxyScopeFormState( val password: String = "", val isPasswordVisible: Boolean = false, val isTestInProgress: Boolean = false, - /** - * True once the user has edited any field in this scope's form. - * Gates the preferences-to-form hydration path in the ViewModel: - * once a scope is dirty, incoming emissions from DataStore are - * ignored for that scope until the user saves (commits) or resets, - * so a concurrent write to *another* preference key doesn't - * clobber the in-progress edit when its Flow re-emits. - */ + val isDraftDirty: Boolean = false, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesViewModel.kt index ec4dcd834..04d75f85e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesViewModel.kt @@ -52,8 +52,7 @@ class SkippedUpdatesViewModel( } private fun unskip(packageName: String) { - // Snapshot the row's display name BEFORE the flow emits without it, - // so the success snackbar can name the app the user just acted on. + val appName = _state.value.items.firstOrNull { it.packageName == packageName }?.appName viewModelScope.launch { try { diff --git a/feature/tweaks/presentation/src/jvmMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.jvm.kt b/feature/tweaks/presentation/src/jvmMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.jvm.kt index 58a1f849b..6f98a1e7d 100644 --- a/feature/tweaks/presentation/src/jvmMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.jvm.kt +++ b/feature/tweaks/presentation/src/jvmMain/kotlin/zed/rainxch/tweaks/presentation/RestartApp.jvm.kt @@ -3,23 +3,6 @@ package zed.rainxch.tweaks.presentation import co.touchlab.kermit.Logger import kotlin.system.exitProcess -/** - * Best-effort "relaunch this JVM" for the Desktop language-change - * flow. In `jpackage`-built installers (DMG/MSI/DEB) the current - * process's command line is a clean invocation of the app launcher, - * and [ProcessHandle] reliably gives us the executable path plus the - * original arguments — `ProcessBuilder` can spawn a fresh instance - * from that and we just exit this one. From IDE runs / `./gradlew - * run` the command line reflects the Gradle-managed forked JVM, - * which may or may not relaunch cleanly depending on classpath and - * stdout wiring; if the spawn fails we still want to exit so the - * user can reopen manually rather than be stuck in a half-applied - * state. - * - * [inheritIO] so the relaunched process shares our stdin/stdout/ - * stderr — mostly relevant in terminal runs; packaged apps have no - * attached terminal so it's a no-op there. - */ actual fun restartAppAfterLanguageChange() { try { val info = ProcessHandle.current().info() @@ -35,9 +18,7 @@ actual fun restartAppAfterLanguageChange() { } } } catch (t: Throwable) { - // Swallow: we'd rather exit cleanly than leave the user in a - // limbo where the app is stuck with the old locale because - // the relaunch errored out. + Logger.w(t) { "restartAppAfterLanguageChange: relaunch failed, falling back to plain exit" } From abd39a08ef1cf65c1d79533a74ed5efb120f96a4 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 21 May 2026 21:08:57 +0500 Subject: [PATCH 030/172] refactor(network): lazy async ProxyManager bootstrap, drop runBlocking DI --- composeApp/build.gradle.kts | 2 - .../rainxch/githubstore/app/di/initKoin.kt | 47 ++++---- .../zed/rainxch/core/data/di/SharedModule.kt | 63 ++++------- .../core}/data/mappers/UserProfileMappers.kt | 4 +- .../core/data/mirror/MirrorRepositoryImpl.kt | 3 +- .../rainxch/core/data/network/ProxyManager.kt | 34 ++++++ .../core/data/network/ProxyManagerSeeding.kt | 3 - .../repository/AnnouncementsRepositoryImpl.kt | 2 +- .../data/repository/CacheRepositoryImpl.kt | 28 +++++ .../repository/HostTokenRepositoryImpl.kt | 2 +- .../repository/UserSessionRepositoryImpl.kt | 67 +++++++++++ .../domain/model/AnnouncementsFeedSnapshot.kt | 27 +++++ .../core/domain/model/MatchingPreview.kt | 7 ++ .../core/domain/model/MirrorRemoved.kt | 5 + .../core/domain/model/TokenValidation.kt | 7 ++ .../rainxch/core}/domain/model/UserProfile.kt | 4 +- .../repository/AnnouncementsRepository.kt | 29 +---- .../core/domain/repository/CacheRepository.kt | 9 ++ .../domain/repository/HostTokenRepository.kt | 9 +- .../repository/InstalledAppsRepository.kt | 9 +- .../domain/repository/MirrorRepository.kt | 5 +- .../repository/UserSessionRepository.kt | 3 + .../composeResources/values/strings.xml | 2 - .../details/presentation/DetailsViewModel.kt | 101 +++++++---------- .../presentation/FavouritesViewModel.kt | 4 +- .../home/presentation/HomeViewModel.kt | 10 +- feature/profile/data/.gitignore | 1 - feature/profile/data/build.gradle.kts | 22 ---- .../data/src/androidMain/AndroidManifest.xml | 4 - .../rainxch/profile/data/di/SharedModule.kt | 19 ---- .../data/repository/ProfileRepositoryImpl.kt | 105 ------------------ feature/profile/domain/.gitignore | 1 - feature/profile/domain/build.gradle.kts | 16 --- .../src/androidMain/AndroidManifest.xml | 4 - .../domain/repository/ProfileRepository.kt | 18 --- feature/profile/presentation/build.gradle.kts | 1 - .../profile/presentation/ProfileState.kt | 2 +- .../profile/presentation/ProfileViewModel.kt | 10 +- .../components/ClearDownloadsDialog.kt | 97 ---------------- .../presentation/components/SectionText.kt | 33 ------ .../components/ToggleSettingCard.kt | 75 ------------- .../components/sections/AccountSection.kt | 2 +- .../profile/presentation/model/ProxyType.kt | 21 ---- .../search/presentation/SearchViewModel.kt | 50 +++++---- feature/starred/presentation/build.gradle.kts | 11 -- .../presentation/StarredReposViewModel.kt | 4 +- .../tweaks/presentation/TweaksViewModel.kt | 12 +- .../feedback/FeedbackViewModel.kt | 10 +- settings.gradle.kts | 2 - 49 files changed, 347 insertions(+), 659 deletions(-) rename {feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile => core/data/src/commonMain/kotlin/zed/rainxch/core}/data/mappers/UserProfileMappers.kt (79%) delete mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt create mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/CacheRepositoryImpl.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AnnouncementsFeedSnapshot.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MatchingPreview.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MirrorRemoved.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TokenValidation.kt rename {feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile => core/domain/src/commonMain/kotlin/zed/rainxch/core}/domain/model/UserProfile.kt (86%) create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/CacheRepository.kt delete mode 100644 feature/profile/data/.gitignore delete mode 100644 feature/profile/data/build.gradle.kts delete mode 100644 feature/profile/data/src/androidMain/AndroidManifest.xml delete mode 100644 feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt delete mode 100644 feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt delete mode 100644 feature/profile/domain/.gitignore delete mode 100644 feature/profile/domain/build.gradle.kts delete mode 100644 feature/profile/domain/src/androidMain/AndroidManifest.xml delete mode 100644 feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ClearDownloadsDialog.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/SectionText.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ToggleSettingCard.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index c65abfdde..d13e44eb5 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -60,8 +60,6 @@ kotlin { implementation(projects.feature.search.data) implementation(projects.feature.search.presentation) - implementation(projects.feature.profile.domain) - implementation(projects.feature.profile.data) implementation(projects.feature.profile.presentation) implementation(projects.feature.starred.domain) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt index 859745de7..79d8b4019 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt @@ -1,5 +1,6 @@ package zed.rainxch.githubstore.app.di +import kotlinx.coroutines.CoroutineScope import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration import zed.rainxch.apps.data.di.appsModule @@ -8,6 +9,8 @@ import zed.rainxch.core.data.di.coreModule import zed.rainxch.core.data.di.corePlatformModule import zed.rainxch.core.data.di.databaseModule import zed.rainxch.core.data.di.networkModule +import zed.rainxch.core.data.network.ProxyManager +import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.details.data.di.detailsModule import zed.rainxch.devprofile.data.di.devProfileModule import zed.rainxch.home.data.di.homeModule @@ -15,23 +18,29 @@ import zed.rainxch.profile.data.di.profileModule import zed.rainxch.search.data.di.searchModule fun initKoin(config: KoinAppDeclaration? = null) { - startKoin { - config?.invoke(this) - modules( - mainModule, - corePlatformModule, - coreModule, - networkModule, - databaseModule, - viewModelsModule, - whatsNewModule, - appsModule, - authModule, - detailsModule, - devProfileModule, - homeModule, - searchModule, - profileModule, - ) - } + val app = + startKoin { + config?.invoke(this) + modules( + mainModule, + corePlatformModule, + coreModule, + networkModule, + databaseModule, + viewModelsModule, + whatsNewModule, + appsModule, + authModule, + detailsModule, + devProfileModule, + homeModule, + searchModule, + profileModule, + ) + } + val koin = app.koin + ProxyManager.bootstrap( + repository = koin.get(), + appScope = koin.get(), + ) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index bdbbdf68f..f0b9b4a6a 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -8,12 +8,6 @@ import io.ktor.http.HttpHeaders import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout import org.koin.core.qualifier.named import org.koin.dsl.module import zed.rainxch.core.data.network.createPlatformHttpClient @@ -47,7 +41,6 @@ import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.MirrorApiClient import zed.rainxch.core.data.network.MockExternalMatchApi import zed.rainxch.core.data.network.ProxyManager -import zed.rainxch.core.data.network.ProxyManagerSeeding import zed.rainxch.core.data.network.ProxyTesterImpl import zed.rainxch.core.data.network.TranslationClientProvider import zed.rainxch.core.data.repository.UserSessionRepositoryImpl @@ -61,6 +54,8 @@ import zed.rainxch.core.data.repository.SeenReposRepositoryImpl import zed.rainxch.core.data.repository.StarredRepositoryImpl import zed.rainxch.core.data.repository.AnnouncementsCacheStoreImpl import zed.rainxch.core.data.repository.AnnouncementsRepositoryImpl +import zed.rainxch.core.data.repository.CacheRepositoryImpl +import zed.rainxch.core.data.repository.HostTokenRepositoryImpl import zed.rainxch.core.data.repository.TweaksRepositoryImpl import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.logging.GitHubStoreLogger @@ -76,6 +71,7 @@ import zed.rainxch.core.domain.system.ExternalAppScanner import zed.rainxch.core.domain.system.MultiSourceDownloader import zed.rainxch.core.domain.repository.AnnouncementsCacheStore import zed.rainxch.core.domain.repository.AnnouncementsRepository +import zed.rainxch.core.domain.repository.CacheRepository import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.repository.ExternalImportRepository import zed.rainxch.core.domain.repository.FavouritesRepository @@ -84,6 +80,7 @@ import zed.rainxch.core.domain.repository.MirrorRepository import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.RateLimitRepository import zed.rainxch.core.domain.repository.HiddenReposRepository +import zed.rainxch.core.domain.repository.HostTokenRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TweaksRepository @@ -106,6 +103,9 @@ val coreModule = single { UserSessionRepositoryImpl( tokenStore = get(), + cacheManager = get(), + clientProvider = get(), + logger = get(), ) } @@ -116,12 +116,20 @@ val coreModule = ) } - single { + single { ForgejoClientRegistry( proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY), ) } + single { + CacheRepositoryImpl( + logger = get(), + fileLocationsProvider = get(), + cacheManager = get() + ) + } + single { InstalledAppsRepositoryImpl( database = get(), @@ -186,7 +194,10 @@ val coreModule = apiClient = get(), appScope = get(), ) - ProxyManager.startMirrorCollector(repo, get()) + ProxyManager.startMirrorCollector( + repository = repo, + scope = get() + ) repo } @@ -231,7 +242,6 @@ val coreModule = } single { - get() BackendApiClient( proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY), tokenStore = get(), @@ -299,31 +309,7 @@ val coreModule = val networkModule = module { - single(createdAtStart = true) { - val repository = get() - runBlocking { - runCatching { - withTimeout(1_500L) { - coroutineScope { - ProxyScope.entries - .map { scope -> - async { - scope to repository.getProxyConfig(scope).first() - } - }.awaitAll() - } - } - }.onSuccess { results -> - results.forEach { (scope, config) -> - ProxyManager.setConfig(scope, config) - } - } - } - ProxyManagerSeeding() - } - - single(createdAtStart = true) { - get() + single { GitHubClientProvider( tokenStore = get(), rateLimitRepository = get(), @@ -332,8 +318,7 @@ val networkModule = ) } - single(createdAtStart = true) { - get() + single { TranslationClientProvider( proxyConfigFlow = ProxyManager.configFlow(ProxyScope.TRANSLATION), ) @@ -350,8 +335,8 @@ val networkModule = RateLimitRepositoryImpl() } - single { - zed.rainxch.core.data.repository.HostTokenRepositoryImpl( + single { + HostTokenRepositoryImpl( ksafe = get(qualifier = named("tokens")), httpClient = get(qualifier = named("test")), ) diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/mappers/UserProfileMappers.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/UserProfileMappers.kt similarity index 79% rename from feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/mappers/UserProfileMappers.kt rename to core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/UserProfileMappers.kt index 3e0df17c5..08a4dc958 100644 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/mappers/UserProfileMappers.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/UserProfileMappers.kt @@ -1,7 +1,7 @@ -package zed.rainxch.profile.data.mappers +package zed.rainxch.core.data.mappers import zed.rainxch.core.data.dto.UserProfileNetwork -import zed.rainxch.profile.domain.model.UserProfile +import zed.rainxch.core.domain.model.UserProfile fun UserProfileNetwork.toUserProfile(): UserProfile = UserProfile( diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mirror/MirrorRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mirror/MirrorRepositoryImpl.kt index fc39e5020..c338e3371 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mirror/MirrorRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mirror/MirrorRepositoryImpl.kt @@ -3,7 +3,6 @@ package zed.rainxch.core.data.mirror import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import eu.anifantakis.lib.ksafe.KSafe @@ -33,12 +32,12 @@ import zed.rainxch.core.domain.model.MirrorPreference import zed.rainxch.core.domain.model.MirrorStatus import zed.rainxch.core.domain.model.MirrorType import zed.rainxch.core.domain.model.TrafficKind -import zed.rainxch.core.domain.repository.MirrorRemoved import zed.rainxch.core.domain.repository.MirrorRepository import zed.rainxch.core.data.secure.safeDelete import zed.rainxch.core.data.secure.safeGet import zed.rainxch.core.data.secure.safeGetFlow import zed.rainxch.core.data.secure.safePut +import zed.rainxch.core.domain.model.MirrorRemoved class MirrorRepositoryImpl( private val ksafe: KSafe, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt index 26eee5310..d8c54d1d0 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt @@ -8,12 +8,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import zed.rainxch.core.domain.model.MirrorPreference import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.core.domain.model.ProxyScope import zed.rainxch.core.domain.model.TrafficKind import zed.rainxch.core.domain.repository.MirrorRepository +import zed.rainxch.core.domain.repository.ProxyRepository data class MirrorActive( val template: String, @@ -27,8 +31,38 @@ object ProxyManager { private val mirror = AtomicReference(null) private var mirrorCollectorJob: Job? = null + private val seedMutex = Mutex() + @Volatile private var seedJob: Job? = null + fun configFlow(scope: ProxyScope): StateFlow = flows.getValue(scope).asStateFlow() + /** + * Idempotent async seed. First call kicks off a background read of every + * [ProxyScope]'s persisted config and pushes it into the flows. Subsequent + * calls no-op (cheap volatile check + Mutex). Flows start at + * [ProxyConfig.System] until the seed completes — typical bootstrap is + * sub-200ms, well before any HTTP traffic. + * + * Replaces the previous `runBlocking` factory in DI. Call once from + * `initKoin()` after `startKoin` returns, fire-and-forget. + */ + fun bootstrap(repository: ProxyRepository, appScope: CoroutineScope) { + if (seedJob != null) return + appScope.launch { + seedMutex.withLock { + if (seedJob != null) return@withLock + seedJob = launch { + ProxyScope.entries.forEach { scope -> + runCatching { + val cfg = repository.getProxyConfig(scope).first() + flows.getValue(scope).value = cfg + } + } + } + } + } + } + fun currentConfig(scope: ProxyScope): ProxyConfig = flows.getValue(scope).value fun setConfig( diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt deleted file mode 100644 index e2793c55d..000000000 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt +++ /dev/null @@ -1,3 +0,0 @@ -package zed.rainxch.core.data.network - -class ProxyManagerSeeding internal constructor() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsRepositoryImpl.kt index a1a7ab76e..e8a70a7f4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/AnnouncementsRepositoryImpl.kt @@ -14,9 +14,9 @@ import zed.rainxch.core.data.services.LocalizationManager import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.Announcement import zed.rainxch.core.domain.model.AnnouncementCategory +import zed.rainxch.core.domain.model.AnnouncementsFeedSnapshot import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.repository.AnnouncementsCacheStore -import zed.rainxch.core.domain.repository.AnnouncementsFeedSnapshot import zed.rainxch.core.domain.repository.AnnouncementsRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.AppVersionInfo diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/CacheRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/CacheRepositoryImpl.kt new file mode 100644 index 000000000..22f0a0c70 --- /dev/null +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/CacheRepositoryImpl.kt @@ -0,0 +1,28 @@ +package zed.rainxch.core.data.repository + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import zed.rainxch.core.data.cache.CacheManager +import zed.rainxch.core.data.services.FileLocationsProvider +import zed.rainxch.core.domain.logging.GitHubStoreLogger +import zed.rainxch.core.domain.repository.CacheRepository + +class CacheRepositoryImpl ( + private val logger: GitHubStoreLogger, + private val fileLocationsProvider: FileLocationsProvider, + private val cacheManager: CacheManager, +) : CacheRepository { + override fun observeCacheSize(): Flow = + flow { + val sizeBytes = fileLocationsProvider.getCacheSizeBytes() + emit(sizeBytes) + }.flowOn(Dispatchers.IO) + + override suspend fun clearCache() { + fileLocationsProvider.clearCacheFiles() + cacheManager.clearAll() + logger.debug("Cache cleared successfully") + } +} \ No newline at end of file diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt index 47fa8e6fd..60d74c303 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HostTokenRepositoryImpl.kt @@ -25,8 +25,8 @@ import kotlinx.serialization.json.Json import kotlin.time.Clock import zed.rainxch.core.domain.model.HostNames import zed.rainxch.core.domain.model.HostToken +import zed.rainxch.core.domain.model.TokenValidation import zed.rainxch.core.domain.repository.HostTokenRepository -import zed.rainxch.core.domain.repository.TokenValidation class HostTokenRepositoryImpl( private val ksafe: KSafe, diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt index ba6e752bf..02ff6e7c4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt @@ -1,14 +1,29 @@ package zed.rainxch.core.data.repository import co.touchlab.kermit.Logger +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import zed.rainxch.core.data.cache.CacheManager +import zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE import zed.rainxch.core.data.data_source.TokenStore +import zed.rainxch.core.data.dto.UserProfileNetwork +import zed.rainxch.core.data.mappers.toUserProfile +import zed.rainxch.core.data.network.GitHubClientProvider +import zed.rainxch.core.data.network.executeRequest +import zed.rainxch.core.domain.logging.GitHubStoreLogger +import zed.rainxch.core.domain.model.UserProfile import zed.rainxch.core.domain.repository.UserSessionRepository import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -16,7 +31,12 @@ import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) class UserSessionRepositoryImpl( private val tokenStore: TokenStore, + private val cacheManager: CacheManager, + private val clientProvider: GitHubClientProvider, + private val logger: GitHubStoreLogger ) : UserSessionRepository { + private val httpClient: HttpClient get() = clientProvider.client + private val _sessionExpiredEvent = MutableSharedFlow(extraBufferCapacity = 1) override val sessionExpiredEvent: SharedFlow = _sessionExpiredEvent.asSharedFlow() @@ -33,6 +53,47 @@ class UserSessionRepositoryImpl( override suspend fun isCurrentlyUserLoggedIn(): Boolean = tokenStore.currentToken() != null + override fun getUser(): Flow = flow { + val token = tokenStore.currentToken() + if (token == null) { + cacheManager.invalidate(CACHE_KEY) + emit(null) + return@flow + } + + val cached = cacheManager.get(CACHE_KEY) + if (cached != null) { + logger.debug("Profile cache hit") + emit(cached) + return@flow + } + + try { + val networkProfile = + httpClient + .executeRequest { + get("/user") { + header(HttpHeaders.Accept, "application/vnd.github+json") + } + }.getOrThrow() + + val userProfile = networkProfile.toUserProfile() + cacheManager.put(CACHE_KEY, userProfile, USER_PROFILE) + logger.debug("Fetched and cached user profile: ${userProfile.username}") + emit(userProfile) + } catch (e: Exception) { + logger.error("Failed to fetch user profile: ${e.message}") + + val stale = cacheManager.getStale(CACHE_KEY) + if (stale != null) { + logger.debug("Using stale cached profile as fallback") + emit(stale) + } else { + emit(null) + } + } + }.flowOn(Dispatchers.IO) + override suspend fun notifySessionExpired(tokenKey: String?) { if (tokenKey.isNullOrEmpty()) return sessionExpiredMutex.withLock { @@ -90,9 +151,15 @@ class UserSessionRepositoryImpl( _consecutiveFailures = 0 } + override suspend fun logout() { + tokenStore.clear() + cacheManager.clearAll() + } + private companion object { const val TAG = "AuthState" const val REQUIRED_CONSECUTIVE_FAILURES = 2 const val FAILURE_WINDOW_MS = 60_000L + private const val CACHE_KEY = "profile:me" } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AnnouncementsFeedSnapshot.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AnnouncementsFeedSnapshot.kt new file mode 100644 index 000000000..a176ad1c2 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AnnouncementsFeedSnapshot.kt @@ -0,0 +1,27 @@ +package zed.rainxch.core.domain.model + +data class AnnouncementsFeedSnapshot( + val items: List, + val dismissedIds: Set, + val acknowledgedIds: Set, + val mutedCategories: Set, + val lastFetchedAtMillis: Long, + val lastRefreshFailed: Boolean, +) { + val visibleItems: List + get() = items + .asSequence() + .filter { it.id !in dismissedIds } + .filter { it.category !in mutedCategories || !it.category.isMutable } + .toList() + + val unreadCount: Int + get() = visibleItems.count { it.id !in acknowledgedIds } + + val pendingCriticalAcknowledgment: Announcement? + get() = visibleItems.firstOrNull { + it.severity == AnnouncementSeverity.CRITICAL && + it.requiresAcknowledgment && + it.id !in acknowledgedIds + } +} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MatchingPreview.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MatchingPreview.kt new file mode 100644 index 000000000..294820fc1 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MatchingPreview.kt @@ -0,0 +1,7 @@ +package zed.rainxch.core.domain.model + +data class MatchingPreview( + val release: GithubRelease?, + val matchedAssets: List, + val regexError: String? = null, +) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MirrorRemoved.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MirrorRemoved.kt new file mode 100644 index 000000000..611ef7203 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/MirrorRemoved.kt @@ -0,0 +1,5 @@ +package zed.rainxch.core.domain.model + +data class MirrorRemoved( + val displayName: String, +) \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TokenValidation.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TokenValidation.kt new file mode 100644 index 000000000..c5a66e4ae --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TokenValidation.kt @@ -0,0 +1,7 @@ +package zed.rainxch.core.domain.model + +data class TokenValidation( + val login: String?, + val scopes: List, + val rateLimitRemaining: Int?, +) diff --git a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/model/UserProfile.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/UserProfile.kt similarity index 86% rename from feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/model/UserProfile.kt rename to core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/UserProfile.kt index 6dbf0dc64..602085fe7 100644 --- a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/model/UserProfile.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/UserProfile.kt @@ -1,4 +1,4 @@ -package zed.rainxch.profile.domain.model +package zed.rainxch.core.domain.model import kotlinx.serialization.Serializable @@ -12,4 +12,4 @@ data class UserProfile( val repositoryCount: Int, val followers: Int, val following: Int, -) +) \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AnnouncementsRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AnnouncementsRepository.kt index 740358b56..f1c0a0306 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AnnouncementsRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/AnnouncementsRepository.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow import zed.rainxch.core.domain.model.Announcement import zed.rainxch.core.domain.model.AnnouncementCategory import zed.rainxch.core.domain.model.AnnouncementSeverity +import zed.rainxch.core.domain.model.AnnouncementsFeedSnapshot interface AnnouncementsRepository { fun observeFeed(): Flow @@ -15,30 +16,4 @@ interface AnnouncementsRepository { suspend fun acknowledge(id: String) suspend fun setMuted(category: AnnouncementCategory, muted: Boolean) -} - -data class AnnouncementsFeedSnapshot( - val items: List, - val dismissedIds: Set, - val acknowledgedIds: Set, - val mutedCategories: Set, - val lastFetchedAtMillis: Long, - val lastRefreshFailed: Boolean, -) { - val visibleItems: List - get() = items - .asSequence() - .filter { it.id !in dismissedIds } - .filter { it.category !in mutedCategories || !it.category.isMutable } - .toList() - - val unreadCount: Int - get() = visibleItems.count { it.id !in acknowledgedIds } - - val pendingCriticalAcknowledgment: Announcement? - get() = visibleItems.firstOrNull { - it.severity == AnnouncementSeverity.CRITICAL && - it.requiresAcknowledgment && - it.id !in acknowledgedIds - } -} +} \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/CacheRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/CacheRepository.kt new file mode 100644 index 000000000..2cc58172f --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/CacheRepository.kt @@ -0,0 +1,9 @@ +package zed.rainxch.core.domain.repository + +import kotlinx.coroutines.flow.Flow + +interface CacheRepository { + fun observeCacheSize(): Flow + + suspend fun clearCache() +} \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HostTokenRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HostTokenRepository.kt index 384c8d859..40c7ff64d 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HostTokenRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HostTokenRepository.kt @@ -2,6 +2,7 @@ package zed.rainxch.core.domain.repository import kotlinx.coroutines.flow.Flow import zed.rainxch.core.domain.model.HostToken +import zed.rainxch.core.domain.model.TokenValidation interface HostTokenRepository { fun observeAll(): Flow> @@ -13,10 +14,4 @@ interface HostTokenRepository { suspend fun delete(host: String) suspend fun validate(host: String, token: String): Result -} - -data class TokenValidation( - val login: String?, - val scopes: List, - val rateLimitRemaining: Int?, -) +} \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt index cc20a729e..47c8111df 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow import zed.rainxch.core.domain.model.GithubAsset import zed.rainxch.core.domain.model.GithubRelease import zed.rainxch.core.domain.model.InstalledApp +import zed.rainxch.core.domain.model.MatchingPreview interface InstalledAppsRepository { fun getAllInstalledApps(): Flow> @@ -108,10 +109,4 @@ interface InstalledAppsRepository { ): MatchingPreview suspend fun executeInTransaction(block: suspend () -> R): R -} - -data class MatchingPreview( - val release: GithubRelease?, - val matchedAssets: List, - val regexError: String? = null, -) +} \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt index f38784d2c..cbf16b1ce 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/MirrorRepository.kt @@ -3,6 +3,7 @@ package zed.rainxch.core.domain.repository import kotlinx.coroutines.flow.Flow import zed.rainxch.core.domain.model.MirrorConfig import zed.rainxch.core.domain.model.MirrorPreference +import zed.rainxch.core.domain.model.MirrorRemoved interface MirrorRepository { @@ -20,7 +21,3 @@ interface MirrorRepository { suspend fun dismissAutoSuggestPermanently() } - -data class MirrorRemoved( - val displayName: String, -) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt index e3066f1b6..4636e89a0 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/UserSessionRepository.kt @@ -2,9 +2,11 @@ package zed.rainxch.core.domain.repository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow +import zed.rainxch.core.domain.model.UserProfile interface UserSessionRepository { fun isUserLoggedIn(): Flow + fun getUser(): Flow suspend fun isCurrentlyUserLoggedIn(): Boolean @@ -13,4 +15,5 @@ interface UserSessionRepository { suspend fun notifySessionExpired(tokenKey: String?) suspend fun notifyRequestSucceeded(tokenKey: String?) + suspend fun logout() } diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 8ac3f8a56..10dc48754 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -792,8 +792,6 @@ All repositories unhidden Could not unhide. Try again. - - Recent searches Clear all diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index f549420a5..95b04536f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -50,6 +50,7 @@ import zed.rainxch.core.domain.system.InstallOutcome import zed.rainxch.core.domain.system.InstallPolicy import zed.rainxch.core.domain.system.Installer import zed.rainxch.core.domain.model.InstallerType +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.system.PackageMonitor import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.util.AssetVariant @@ -100,12 +101,12 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Clock.System import kotlin.time.ExperimentalTime +import kotlin.time.Instant class DetailsViewModel( private val repositoryId: Long, private val ownerParam: String, private val repoParam: String, - private val sourceHostParam: String?, private val detailsRepository: DetailsRepository, private val downloader: Downloader, @@ -128,9 +129,8 @@ class DetailsViewModel( private val downloadOrchestrator: DownloadOrchestrator, private val externalImportRepository: ExternalImportRepository, private val apkInspector: ApkInspector, - private val userSessionRepository: zed.rainxch.core.domain.repository.UserSessionRepository, private val systemInstallSerializer: zed.rainxch.core.domain.system.SystemInstallSerializer, - private val profileRepository: zed.rainxch.profile.domain.repository.ProfileRepository, + private val userSessionRepository: UserSessionRepository ) : ViewModel() { private var hasLoadedInitialData = false private var currentDownloadJob: Job? = null @@ -597,7 +597,7 @@ class DetailsViewModel( val latestStableHasInstallableAsset: Boolean, ) - @OptIn(kotlin.time.ExperimentalTime::class) + @OptIn(ExperimentalTime::class) private fun computeReleaseInsights( allReleases: List, installedApp: InstalledApp?, @@ -636,7 +636,7 @@ class DetailsViewModel( val preReleasesAfter = allReleases.any { release -> release.isEffectivelyPreRelease() && - VersionMath.isVersionNewer(release.tagName, stable.tagName) + VersionMath.isVersionNewer(release.tagName, stable.tagName) } if (!preReleasesAfter) return@run null val days = daysSinceIso(stable.publishedAt) ?: return@run null @@ -654,15 +654,15 @@ class DetailsViewModel( ) } - @OptIn(kotlin.time.ExperimentalTime::class) + @OptIn(ExperimentalTime::class) private fun daysSinceIso(isoTimestamp: String?): Int? { if (isoTimestamp.isNullOrBlank()) return null return try { - val published = kotlin.time.Instant.parse(isoTimestamp) + val published = Instant.parse(isoTimestamp) val now = System.now() val diffMs = now.toEpochMilliseconds() - published.toEpochMilliseconds() if (diffMs < 0) null else (diffMs / MILLIS_PER_DAY).toInt() - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -724,15 +724,15 @@ class DetailsViewModel( } val isSameFingerprint = sameVariant && - serializedTokens == currentTokens && - fingerprint.glob == currentGlob && - pickedIndex == installedApp.pickedAssetIndex && - newSiblingCount == installedApp.pickedAssetSiblingCount + serializedTokens == currentTokens && + fingerprint.glob == currentGlob && + pickedIndex == installedApp.pickedAssetIndex && + newSiblingCount == installedApp.pickedAssetSiblingCount val isFirstPin = currentVariant.isNullOrBlank() && - currentTokens.isNullOrBlank() && - currentGlob.isNullOrBlank() + currentTokens.isNullOrBlank() && + currentGlob.isNullOrBlank() val shouldSave = !isSameFingerprint || installedApp.preferredVariantStale if (!shouldSave) return @@ -764,7 +764,7 @@ class DetailsViewModel( } catch (e: Exception) { logger.error( "Failed to persist preferred variant for " + - "${installedApp.packageName}: ${e.message}", + "${installedApp.packageName}: ${e.message}", ) } } @@ -783,7 +783,7 @@ class DetailsViewModel( } catch (e: Exception) { logger.error( "Failed to clear preferred variant for " + - "${installedApp.packageName}: ${e.message}", + "${installedApp.packageName}: ${e.message}", ) } } @@ -792,7 +792,7 @@ class DetailsViewModel( private fun observeCurrentUserForBadge() { viewModelScope.launch { combine( - profileRepository.getUser(), + userSessionRepository.getUser(), _state .map { it.repository?.owner?.login } .distinctUntilChanged(), @@ -923,20 +923,18 @@ class DetailsViewModel( installer.isAssetInstallable(asset.name) }.orEmpty() - val trackedApp = installedAppOverride - val variantMatch = - AssetVariant.resolvePreferredAsset( - assets = installable, - pinnedVariant = trackedApp?.preferredAssetVariant, - pinnedTokens = AssetVariant.deserializeTokens(trackedApp?.preferredAssetTokens), - pinnedGlob = trackedApp?.assetGlobPattern, - ) + val variantMatch = AssetVariant.resolvePreferredAsset( + assets = installable, + pinnedVariant = installedAppOverride?.preferredAssetVariant, + pinnedTokens = AssetVariant.deserializeTokens(installedAppOverride?.preferredAssetTokens), + pinnedGlob = installedAppOverride?.assetGlobPattern, + ) val samePositionMatch = if (variantMatch == null) { AssetVariant.resolveBySamePosition( assets = installable, - originalIndex = trackedApp?.pickedAssetIndex, - siblingCountAtPickTime = trackedApp?.pickedAssetSiblingCount, + originalIndex = installedAppOverride?.pickedAssetIndex, + siblingCountAtPickTime = installedAppOverride?.pickedAssetSiblingCount, ) } else { null @@ -1253,8 +1251,6 @@ class DetailsViewModel( private fun openApp() { val installedApp = _state.value.installedApp ?: return val launched = installer.openApp(installedApp.packageName) - if (launched && platform == Platform.ANDROID) { - } if (!launched) { viewModelScope.launch { _events.send( @@ -1341,10 +1337,6 @@ class DetailsViewModel( val newFavoriteState = favouritesRepository.isFavoriteSync(repo.id) _state.value = _state.value.copy(isFavourite = newFavoriteState) - if (newFavoriteState) { - } else { - } - _events.send( element = DetailsEvent.OnMessage( @@ -1606,7 +1598,7 @@ class DetailsViewModel( tweaksRepository.getInstallerType().first() } catch (e: kotlinx.coroutines.CancellationException) { throw e - } catch (e: Exception) { + } catch (_: Exception) { InstallerType.DEFAULT } val policy = @@ -1706,7 +1698,6 @@ class DetailsViewModel( isUpdate: Boolean, ) { var installFired = false - var telemetryStartFired = false downloadOrchestrator.observe(packageKey).collect { entry -> if (entry == null) { @@ -1739,14 +1730,7 @@ class DetailsViewModel( } OrchestratorStage.Installing -> { - _state.value = _state.value.copy(downloadStage = DownloadStage.INSTALLING) - - if (!telemetryStartFired) { - telemetryStartFired = true - _state.value.repository?.id?.let { id -> - } - } } OrchestratorStage.AwaitingInstall -> { @@ -1762,11 +1746,6 @@ class DetailsViewModel( tag = releaseTag, result = LogResult.Downloaded, ) - if (!telemetryStartFired) { - telemetryStartFired = true - _state.value.repository?.id?.let { id -> - } - } try { installAsset( @@ -1794,8 +1773,6 @@ class DetailsViewModel( tag = releaseTag, result = Error(t.message), ) - _state.value.repository?.id?.let { - } } } @@ -1815,10 +1792,6 @@ class DetailsViewModel( else -> LogResult.Installed }, ) - if (isCompleted) { - _state.value.repository?.id?.let { - } - } if (platform == Platform.ANDROID) { val filePath = entry.filePath @@ -1843,7 +1816,7 @@ class DetailsViewModel( } else { logger.warn( "Orchestrator install settled (outcome=$resolvedOutcome) " + - "but APK validation failed: $validation", + "but APK validation failed: $validation", ) } }.onFailure { t -> @@ -1852,7 +1825,7 @@ class DetailsViewModel( } else { logger.warn( "Orchestrator install settled (outcome=$resolvedOutcome) " + - "but filePath is null; DB not updated", + "but filePath is null; DB not updated", ) } } @@ -1928,15 +1901,15 @@ class DetailsViewModel( logger.warn( "Could not extract APK info for $assetName, " + - "proceeding with unvalidated install", + "proceeding with unvalidated install", ) } is ApkValidationResult.PackageMismatch -> { logger.error( "Package name mismatch on update: " + - "APK=${validationResult.apkPackageName}, " + - "installed=${validationResult.installedPackageName}", + "APK=${validationResult.apkPackageName}, " + + "installed=${validationResult.installedPackageName}", ) _state.value = _state.value.copy( @@ -2191,7 +2164,7 @@ class DetailsViewModel( assetName = assetName, size = sizeBytes, tag = releaseTag, - result = LogResult.Error(t.message), + result = Error(t.message), ) } } @@ -2278,12 +2251,14 @@ class DetailsViewModel( sourceHost = sourceHostParam, ) } + ownerParam.isNotEmpty() && repoParam.isNotEmpty() -> detailsRepository.getRepositoryByOwnerAndName( owner = ownerParam, name = repoParam, sourceHost = null, ) + else -> detailsRepository.getRepositoryById(repositoryId) } launch { seenReposRepository.markAsSeen(repo) } @@ -2446,7 +2421,10 @@ class DetailsViewModel( allReleases.firstOrNull { !it.isEffectivelyPreRelease() } ?: allReleases.firstOrNull() - val (installable, primary) = recomputeAssetsForRelease(selectedRelease, installedApp) + val (installable, primary) = recomputeAssetsForRelease( + selectedRelease, + installedApp + ) val isObtainiumAvailable = installer.isObtainiumInstalled() val isAppManagerAvailable = installer.isAppManagerInstalled() @@ -2703,10 +2681,9 @@ class DetailsViewModel( ) } - val releaseSourceLang = currentReadmeLang if (!releaseDescription.isNullOrBlank() && _state.value.whatsNewTranslation.translatedText == null && - releaseSourceLang?.equals(target, ignoreCase = true) != true + currentReadmeLang?.equals(target, ignoreCase = true) != true ) { whatsNewTranslationJob?.cancel() whatsNewTranslationJob = translateContent( diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt index 65b15de70..2359885b9 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.launch import zed.rainxch.core.domain.model.FavoriteRepo import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.favourites.presentation.mappers.toFavouriteRepositoryUi import zed.rainxch.favourites.presentation.model.FavouritesSortRule import kotlin.time.Clock @@ -24,6 +25,7 @@ import kotlin.time.ExperimentalTime class FavouritesViewModel( private val favouritesRepository: FavouritesRepository, private val tweaksRepository: TweaksRepository, + private val userSessionRepository: UserSessionRepository ) : ViewModel() { private var hasLoadedInitialData = false @@ -46,7 +48,7 @@ class FavouritesViewModel( viewModelScope.launch { combine( favouritesRepository.getAllFavorites(), - profileRepository.getUser(), + userSessionRepository.getUser(), tweaksRepository.getFavouritesSortRule(), ) { favorites, user, sortStored -> val sortRule = FavouritesSortRule.fromName(sortStored) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 9e7705d78..9f7b4f665 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -34,13 +34,13 @@ import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ShareManager import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.utils.toUi import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.home.domain.repository.HomeRepository -import zed.rainxch.profile.domain.repository.ProfileRepository class HomeViewModel( private val homeRepository: HomeRepository, @@ -48,12 +48,12 @@ class HomeViewModel( private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, private val favouritesRepository: FavouritesRepository, private val starredRepository: StarredRepository, - private val logger: GitHubStoreLogger, - private val shareManager: ShareManager, private val tweaksRepository: TweaksRepository, private val seenReposRepository: SeenReposRepository, private val hiddenReposRepository: HiddenReposRepository, - private val profileRepository: ProfileRepository, + private val userSessionRepository: UserSessionRepository, + private val logger: GitHubStoreLogger, + private val shareManager: ShareManager, ) : ViewModel() { private var hasLoadedInitialData = false private var loadJob: Job? = null @@ -405,7 +405,7 @@ class HomeViewModel( private fun observeCurrentUser() { viewModelScope.launch { - profileRepository.getUser().collect { user -> + userSessionRepository.getUser().collect { user -> currentUserLogin = user?.username val signedIn = user != null val previouslySignedIn = _state.value.isUserSignedIn diff --git a/feature/profile/data/.gitignore b/feature/profile/data/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/feature/profile/data/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/profile/data/build.gradle.kts b/feature/profile/data/build.gradle.kts deleted file mode 100644 index 4040b24de..000000000 --- a/feature/profile/data/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -plugins { - alias(libs.plugins.convention.kmp.library) - alias(libs.plugins.convention.buildkonfig) -} - -kotlin { - sourceSets { - commonMain { - dependencies { - implementation(libs.kotlin.stdlib) - implementation(libs.kotlinx.coroutines.core) - - implementation(projects.core.domain) - implementation(projects.core.data) - implementation(projects.feature.profile.domain) - - implementation(libs.bundles.koin.common) - implementation(libs.bundles.ktor.common) - } - } - } -} diff --git a/feature/profile/data/src/androidMain/AndroidManifest.xml b/feature/profile/data/src/androidMain/AndroidManifest.xml deleted file mode 100644 index a5918e68a..000000000 --- a/feature/profile/data/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt deleted file mode 100644 index f285e7742..000000000 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package zed.rainxch.profile.data.di - -import org.koin.dsl.module -import zed.rainxch.profile.data.repository.ProfileRepositoryImpl -import zed.rainxch.profile.domain.repository.ProfileRepository - -val profileModule = - module { - single { - ProfileRepositoryImpl( - userSessionRepository = get(), - tokenStore = get(), - clientProvider = get(), - cacheManager = get(), - logger = get(), - fileLocationsProvider = get(), - ) - } - } diff --git a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt b/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt deleted file mode 100644 index 30139cc6a..000000000 --- a/feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt +++ /dev/null @@ -1,105 +0,0 @@ -package zed.rainxch.profile.data.repository - -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.http.HttpHeaders -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import zed.rainxch.core.data.cache.CacheManager -import zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE -import zed.rainxch.core.data.data_source.TokenStore -import zed.rainxch.core.data.dto.UserProfileNetwork -import zed.rainxch.core.data.network.GitHubClientProvider -import zed.rainxch.core.data.network.executeRequest -import zed.rainxch.core.data.services.FileLocationsProvider -import zed.rainxch.core.domain.logging.GitHubStoreLogger -import zed.rainxch.core.domain.repository.UserSessionRepository -import zed.rainxch.feature.profile.data.BuildKonfig -import zed.rainxch.profile.data.mappers.toUserProfile -import zed.rainxch.profile.domain.model.UserProfile -import zed.rainxch.profile.domain.repository.ProfileRepository - -class ProfileRepositoryImpl( - private val userSessionRepository: UserSessionRepository, - private val tokenStore: TokenStore, - private val clientProvider: GitHubClientProvider, - private val cacheManager: CacheManager, - private val logger: GitHubStoreLogger, - private val fileLocationsProvider: FileLocationsProvider, -) : ProfileRepository { - private val httpClient: HttpClient get() = clientProvider.client - - companion object { - private const val CACHE_KEY = "profile:me" - } - - override val isUserLoggedIn: Flow - get() = - userSessionRepository - .isUserLoggedIn() - .flowOn(Dispatchers.IO) - - override fun getUser(): Flow = - flow { - val token = tokenStore.currentToken() - if (token == null) { - cacheManager.invalidate(CACHE_KEY) - emit(null) - return@flow - } - - val cached = cacheManager.get(CACHE_KEY) - if (cached != null) { - logger.debug("Profile cache hit") - emit(cached) - return@flow - } - - try { - val networkProfile = - httpClient - .executeRequest { - get("/user") { - header(HttpHeaders.Accept, "application/vnd.github+json") - } - }.getOrThrow() - - val userProfile = networkProfile.toUserProfile() - cacheManager.put(CACHE_KEY, userProfile, USER_PROFILE) - logger.debug("Fetched and cached user profile: ${userProfile.username}") - emit(userProfile) - } catch (e: Exception) { - logger.error("Failed to fetch user profile: ${e.message}") - - val stale = cacheManager.getStale(CACHE_KEY) - if (stale != null) { - logger.debug("Using stale cached profile as fallback") - emit(stale) - } else { - emit(null) - } - } - }.flowOn(Dispatchers.IO) - - override fun getVersionName(): String = BuildKonfig.VERSION_NAME - - override suspend fun logout() { - tokenStore.clear() - cacheManager.clearAll() - } - - override fun observeCacheSize(): Flow = - flow { - val sizeBytes = fileLocationsProvider.getCacheSizeBytes() - emit(sizeBytes) - }.flowOn(Dispatchers.IO) - - override suspend fun clearCache() { - fileLocationsProvider.clearCacheFiles() - cacheManager.clearAll() - logger.debug("Cache cleared successfully") - } -} diff --git a/feature/profile/domain/.gitignore b/feature/profile/domain/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/feature/profile/domain/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/profile/domain/build.gradle.kts b/feature/profile/domain/build.gradle.kts deleted file mode 100644 index 4a38ddd7f..000000000 --- a/feature/profile/domain/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - alias(libs.plugins.convention.kmp.library) -} - -kotlin { - sourceSets { - commonMain { - dependencies { - implementation(libs.kotlin.stdlib) - implementation(libs.kotlinx.coroutines.core) - - implementation(projects.core.domain) - } - } - } -} diff --git a/feature/profile/domain/src/androidMain/AndroidManifest.xml b/feature/profile/domain/src/androidMain/AndroidManifest.xml deleted file mode 100644 index a5918e68a..000000000 --- a/feature/profile/domain/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt b/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt deleted file mode 100644 index c4fa64a15..000000000 --- a/feature/profile/domain/src/commonMain/kotlin/zed/rainxch/profile/domain/repository/ProfileRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package zed.rainxch.profile.domain.repository - -import kotlinx.coroutines.flow.Flow -import zed.rainxch.profile.domain.model.UserProfile - -interface ProfileRepository { - val isUserLoggedIn: Flow - - fun getUser(): Flow - - fun getVersionName(): String - - suspend fun logout() - - fun observeCacheSize(): Flow - - suspend fun clearCache() -} diff --git a/feature/profile/presentation/build.gradle.kts b/feature/profile/presentation/build.gradle.kts index add8825be..93a21587f 100644 --- a/feature/profile/presentation/build.gradle.kts +++ b/feature/profile/presentation/build.gradle.kts @@ -10,7 +10,6 @@ kotlin { implementation(projects.core.domain) implementation(projects.core.presentation) - implementation(projects.feature.profile.domain) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt index b03df8998..bac4583f0 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt @@ -1,6 +1,6 @@ package zed.rainxch.profile.presentation -import zed.rainxch.profile.domain.model.UserProfile +import zed.rainxch.core.domain.model.UserProfile data class ProfileState( val userProfile: UserProfile? = null, diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 661d81be7..0f0c42184 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -11,10 +11,10 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import zed.rainxch.profile.domain.repository.ProfileRepository +import zed.rainxch.core.domain.repository.UserSessionRepository class ProfileViewModel( - private val profileRepository: ProfileRepository, + private val userSessionRepository: UserSessionRepository ) : ViewModel() { private var userProfileJob: Job? = null @@ -56,7 +56,7 @@ class ProfileViewModel( private fun observeLoggedInStatus() { viewModelScope.launch { - profileRepository.isUserLoggedIn + userSessionRepository.isUserLoggedIn() .collect { isLoggedIn -> _state.update { it.copy(isUserLoggedIn = isLoggedIn) } if (isLoggedIn) { @@ -73,7 +73,7 @@ class ProfileViewModel( userProfileJob = viewModelScope.launch { - profileRepository.getUser().collect { profile -> + userSessionRepository.getUser().collect { profile -> _state.update { it.copy(userProfile = profile) } } } @@ -92,7 +92,7 @@ class ProfileViewModel( ProfileAction.OnLogoutConfirmClick -> { viewModelScope.launch { runCatching { - profileRepository.logout() + userSessionRepository.logout() }.onSuccess { _state.update { it.copy(isLogoutDialogVisible = false, userProfile = null) } _events.send(ProfileEvent.OnLogoutSuccessful) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ClearDownloadsDialog.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ClearDownloadsDialog.kt deleted file mode 100644 index d34596874..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ClearDownloadsDialog.kt +++ /dev/null @@ -1,97 +0,0 @@ -package zed.rainxch.profile.presentation.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ClearDownloadsDialog( - cacheSize: String, - onDismissRequest: () -> Unit, - onConfirm: () -> Unit, - modifier: Modifier = Modifier, -) { - BasicAlertDialog( - onDismissRequest = onDismissRequest, - properties = - DialogProperties( - dismissOnClickOutside = true, - usePlatformDefaultWidth = false, - ), - modifier = - modifier - .padding(16.dp) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(16.dp), - ) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(Res.string.delete_downloads_confirmation_title), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - ) - - Text( - text = stringResource(Res.string.delete_downloads_confirmation_message, cacheSize), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), - ) { - TextButton( - onClick = onDismissRequest, - ) { - Text( - text = stringResource(Res.string.cancel), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - - Button( - onClick = onConfirm, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Text( - text = stringResource(Res.string.delete_all), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - ) - } - } - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/SectionText.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/SectionText.kt deleted file mode 100644 index e3a6ed2a5..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/SectionText.kt +++ /dev/null @@ -1,33 +0,0 @@ -package zed.rainxch.profile.presentation.components - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun SectionTitle(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleMediumEmphasized, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp), - ) -} - -@Composable -fun SectionHeader(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp), - ) -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ToggleSettingCard.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ToggleSettingCard.kt deleted file mode 100644 index 6622db1aa..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ToggleSettingCard.kt +++ /dev/null @@ -1,75 +0,0 @@ -package zed.rainxch.profile.presentation.components - -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.toggleable -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import zed.rainxch.core.presentation.components.ExpressiveCard - -@Composable -fun ToggleSettingCard( - title: String, - description: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, -) { - ExpressiveCard { - Row( - modifier = - Modifier - .fillMaxWidth() - .toggleable( - value = checked, - onValueChange = onCheckedChange, - role = Role.Switch, - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(), - ).padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = - Modifier - .weight(1f) - .padding(end = 16.dp), - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - Switch( - checked = checked, - onCheckedChange = null, - ) - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt index 2c07f063e..c7cb46dd0 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt @@ -36,7 +36,7 @@ import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.domain.model.UserProfile +import zed.rainxch.core.domain.model.UserProfile import zed.rainxch.profile.presentation.ProfileAction import zed.rainxch.profile.presentation.ProfileState diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt deleted file mode 100644 index 5581a719a..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/model/ProxyType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package zed.rainxch.profile.presentation.model - -import zed.rainxch.core.domain.model.ProxyConfig - -enum class ProxyType { - NONE, - SYSTEM, - HTTP, - SOCKS, - ; - - companion object { - fun fromConfig(config: ProxyConfig): ProxyType = - when (config) { - is ProxyConfig.None -> NONE - is ProxyConfig.System -> SYSTEM - is ProxyConfig.Http -> HTTP - is ProxyConfig.Socks -> SOCKS - } - } -} diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 3183d5cbe..eb15dd200 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -30,6 +30,7 @@ import zed.rainxch.domain.repository.SearchHistoryRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ClipboardHelper import zed.rainxch.core.domain.utils.ShareManager @@ -62,7 +63,7 @@ class SearchViewModel( private val tweaksRepository: TweaksRepository, private val seenReposRepository: SeenReposRepository, private val searchHistoryRepository: SearchHistoryRepository, - private val profileRepository: ProfileRepository, + private val userSessionRepository: UserSessionRepository, private val hiddenReposRepository: HiddenReposRepository, private val initialPlatform: SearchPlatformUi? = null, ) : ViewModel() { @@ -72,7 +73,8 @@ class SearchViewModel( private var explorePage = 1 private var lastExploreQuery = "" - @Volatile private var currentUserLogin: String? = null + @Volatile + private var currentUserLogin: String? = null private val exploreLog = logger.withTag("SearchExplore") @@ -120,7 +122,7 @@ class SearchViewModel( return state.repositories .filter { repo -> repo.repository.id !in hidden && - (!needsHideSeenFilter || repo.repository.id !in state.seenRepoIds) + (!needsHideSeenFilter || repo.repository.id !in state.seenRepoIds) } .toImmutableList() } @@ -161,14 +163,15 @@ class SearchViewModel( .filter { it.isNotBlank() && !it.equals("codeberg.org", ignoreCase = true) } .sorted() .map { zed.rainxch.search.presentation.model.SearchSourceUi.CustomForge(it) } - val all = kotlinx.collections.immutable.persistentListOf() - .addAll(base + extra) + val all = + kotlinx.collections.immutable.persistentListOf() + .addAll(base + extra) _state.update { current -> - val current_sel = current.selectedSource - val stillValid = all.contains(current_sel) + val currentSel = current.selectedSource + val stillValid = all.contains(currentSel) current.copy( availableSources = all, - selectedSource = if (stillValid) current_sel + selectedSource = if (stillValid) currentSel else zed.rainxch.search.presentation.model.SearchSourceUi.GitHub, ) } @@ -186,7 +189,7 @@ class SearchViewModel( private fun observeCurrentUser() { viewModelScope.launch { - profileRepository.getUser().collect { user -> + userSessionRepository.getUser().collect { user -> currentUserLogin = user?.username val login = user?.username _state.update { current -> @@ -195,11 +198,14 @@ class SearchViewModel( current.repositories .map { repo -> repo.copy( - isCurrentUserOwner = - login != null && - repo.repository.owner.login.equals(login, ignoreCase = true), + isCurrentUserOwner = login != null && + repo.repository.owner.login.equals( + login, + ignoreCase = true + ), ) - }.toImmutableList(), + } + .toImmutableList(), ) } } @@ -432,7 +438,10 @@ class SearchViewModel( isSeen = repo.id in seenIds, isCurrentUserOwner = currentLogin != null && - repo.owner.login.equals(currentLogin, ignoreCase = true), + repo.owner.login.equals( + currentLogin, + ignoreCase = true + ), isUpdateAvailable = apps.any { it.hasActualUpdate() }, repository = repo.toUi(), ) @@ -835,7 +844,7 @@ class SearchViewModel( exploreLog.debug( "click: query='$query' platform=$platformUi " + - "page=$explorePage lastQuery='$lastExploreQuery' status=$prevStatus", + "page=$explorePage lastQuery='$lastExploreQuery' status=$prevStatus", ) if (query.isBlank()) { @@ -867,8 +876,8 @@ class SearchViewModel( val existingCount = _state.value.repositories.size exploreLog.debug( "response: items=${exploreResult.repos.size} " + - "returnedPage=${exploreResult.page} hasMore=${exploreResult.hasMore} " + - "existingVisible=$existingCount", + "returnedPage=${exploreResult.page} hasMore=${exploreResult.hasMore} " + + "existingVisible=$existingCount", ) val before = _state.value.repositories.size @@ -887,7 +896,7 @@ class SearchViewModel( } else { exploreLog.debug( "-> EXHAUSTED: appended=$added dupes=$dupes " + - "rawItems=${exploreResult.repos.size}", + "rawItems=${exploreResult.repos.size}", ) _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.EXHAUSTED) } } @@ -904,7 +913,8 @@ class SearchViewModel( private suspend fun appendExploreResults( newRepos: List, ) { - val installedMap = installedAppsRepository.getAllInstalledApps().first().groupBy { it.repoId } + val installedMap = + installedAppsRepository.getAllInstalledApps().first().groupBy { it.repoId } val favoritesMap = favouritesRepository.getAllFavorites().first().associateBy { it.repoId } val starredMap = starredRepository.getAllStarred().first().associateBy { it.repoId } val seenIds = _state.value.seenRepoIds @@ -923,7 +933,7 @@ class SearchViewModel( isSeen = repo.id in seenIds, isCurrentUserOwner = currentLogin != null && - repo.owner.login.equals(currentLogin, ignoreCase = true), + repo.owner.login.equals(currentLogin, ignoreCase = true), isUpdateAvailable = apps.any { it.hasActualUpdate() }, repository = repo.toUi(), ) diff --git a/feature/starred/presentation/build.gradle.kts b/feature/starred/presentation/build.gradle.kts index 66efd49b3..2279634a9 100644 --- a/feature/starred/presentation/build.gradle.kts +++ b/feature/starred/presentation/build.gradle.kts @@ -12,7 +12,6 @@ kotlin { implementation(projects.core.domain) implementation(projects.core.presentation) implementation(projects.feature.starred.domain) - implementation(projects.feature.profile.domain) implementation(libs.bundles.landscapist) @@ -20,15 +19,5 @@ kotlin { implementation(libs.jetbrains.compose.components.resources) } } - - androidMain { - dependencies { - } - } - - jvmMain { - dependencies { - } - } } } diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt index 0bf027b23..dd61610b6 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt @@ -22,7 +22,6 @@ import zed.rainxch.core.domain.repository.FavouritesRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.domain.repository.ProfileRepository import zed.rainxch.starred.presentation.mappers.toStarredRepositoryUi import zed.rainxch.starred.presentation.model.StarredRepositoryUi import zed.rainxch.starred.presentation.model.StarredSortRule @@ -33,7 +32,6 @@ class StarredReposViewModel( private val userSessionRepository: UserSessionRepository, private val starredRepository: StarredRepository, private val favouritesRepository: FavouritesRepository, - private val profileRepository: ProfileRepository, private val tweaksRepository: TweaksRepository, ) : ViewModel() { private var hasLoadedInitialData = false @@ -70,7 +68,7 @@ class StarredReposViewModel( combine( starredRepository.getAllStarred(), favouritesRepository.getAllFavorites(), - profileRepository.getUser(), + userSessionRepository.getUser(), tweaksRepository.getStarredSortRule(), ) { starred, favorites, user, sortStored -> val sortRule = StarredSortRule.fromName(sortStored) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 401cdfaea..e98e9af24 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -29,6 +29,7 @@ import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.system.AggressiveOemDetector +import zed.rainxch.core.domain.system.AppVersionInfo import zed.rainxch.core.domain.system.InstallerStatusProvider import zed.rainxch.core.domain.system.UpdateScheduleManager import zed.rainxch.tweaks.presentation.model.ProxyScopeFormState @@ -43,12 +44,11 @@ import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_status import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_timeout import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_unknown import zed.rainxch.githubstore.core.presentation.res.proxy_test_error_unreachable -import zed.rainxch.profile.domain.repository.ProfileRepository import zed.rainxch.tweaks.presentation.model.ProxyType class TweaksViewModel( private val tweaksRepository: TweaksRepository, - private val profileRepository: ProfileRepository, + private val appVersionInfo: AppVersionInfo, private val installerStatusProvider: InstallerStatusProvider, private val proxyRepository: ProxyRepository, private val proxyTester: ProxyTester, @@ -146,13 +146,7 @@ class TweaksViewModel( } private fun loadVersionName() { - viewModelScope.launch { - _state.update { - it.copy( - versionName = profileRepository.getVersionName(), - ) - } - } + _state.update { it.copy(versionName = appVersionInfo.versionName) } } private fun loadCurrentTheme() { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt index 9e1e859aa..a8cc921d5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt @@ -17,8 +17,9 @@ import zed.rainxch.core.domain.getSystemLocaleTag import zed.rainxch.core.domain.model.InstallerType import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository +import zed.rainxch.core.domain.system.AppVersionInfo import zed.rainxch.core.domain.utils.BrowserHelper -import zed.rainxch.profile.domain.repository.ProfileRepository import zed.rainxch.tweaks.presentation.feedback.model.DiagnosticsInfo import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel import zed.rainxch.tweaks.presentation.feedback.util.FeedbackComposer @@ -26,7 +27,8 @@ import zed.rainxch.tweaks.presentation.feedback.util.FeedbackComposer class FeedbackViewModel( private val browserHelper: BrowserHelper, private val tweaksRepository: TweaksRepository, - private val profileRepository: ProfileRepository, + private val userSessionRepository: UserSessionRepository, + private val appVersionInfo: AppVersionInfo, ) : ViewModel() { private val _state = MutableStateFlow(FeedbackState()) val state = _state.asStateFlow() @@ -114,10 +116,10 @@ class FeedbackViewModel( } else { null } - val user = profileRepository.getUser().firstOrNull() + val user = userSessionRepository.getUser().firstOrNull() val appLanguage = tweaksRepository.getAppLanguage().firstOrNull() return DiagnosticsInfo( - appVersion = profileRepository.getVersionName(), + appVersion = appVersionInfo.versionName, platform = platform.displayName(), osVersion = getOsVersion(), locale = appLanguage ?: getSystemLocaleTag(), diff --git a/settings.gradle.kts b/settings.gradle.kts index 866bf567b..4a7f63eac 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,6 +64,4 @@ include(":feature:starred:presentation") include(":feature:search:domain") include(":feature:search:data") include(":feature:search:presentation") -include(":feature:profile:domain") -include(":feature:profile:data") include(":feature:profile:presentation") From cfb29db4159970de8db944e85ccfe2742ca1cabc Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Fri, 22 May 2026 11:17:59 +0500 Subject: [PATCH 031/172] refactor(home): viewmodel-derived HomeRepoCardUi, inline composables, no calc in UI --- .../announcements/AnnouncementsViewModel.kt | 5 +- .../githubstore/app/di/ViewModelsModule.kt | 22 +- .../rainxch/githubstore/app/di/initKoin.kt | 2 - .../zed/rainxch/core/data/di/SharedModule.kt | 2 +- .../rainxch/core/data/network/ProxyManager.kt | 10 - .../repository/InstalledAppsRepositoryImpl.kt | 2 +- .../repository/UserSessionRepositoryImpl.kt | 5 +- .../zed/rainxch/home/presentation/HomeRoot.kt | 457 ++++++-------- .../rainxch/home/presentation/HomeState.kt | 17 +- .../home/presentation/HomeViewModel.kt | 563 +++++++++--------- .../categorylist/CategoryListRoot.kt | 14 +- .../categorylist/CategoryListState.kt | 4 +- .../categorylist/CategoryListViewModel.kt | 16 +- .../components/HomeTimeHelpers.kt | 37 -- .../presentation/components/HotCardItem.kt | 95 +-- .../home/presentation/components/LeadCard.kt | 120 ++-- .../components/PlatformFilterMenu.kt | 74 --- .../presentation/components/StarredRowItem.kt | 27 +- .../components/TrendingRowItem.kt | 44 +- .../presentation/model/HomeRepoCardMapper.kt | 94 +++ .../home/presentation/model/HomeRepoCardUi.kt | 35 ++ .../profile/presentation/ProfileRoot.kt | 10 +- .../tweaks/presentation/TweaksViewModel.kt | 7 +- 23 files changed, 768 insertions(+), 894 deletions(-) delete mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt delete mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/announcements/AnnouncementsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/announcements/AnnouncementsViewModel.kt index 5090e01bb..aeab87f76 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/announcements/AnnouncementsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/announcements/AnnouncementsViewModel.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalTime::class) + package zed.rainxch.githubstore.app.announcements import androidx.lifecycle.ViewModel @@ -15,9 +17,10 @@ import zed.rainxch.core.domain.model.Announcement import zed.rainxch.core.domain.model.AnnouncementCategory import zed.rainxch.core.domain.model.AnnouncementIconHint import zed.rainxch.core.domain.model.AnnouncementSeverity -import zed.rainxch.core.domain.repository.AnnouncementsFeedSnapshot +import zed.rainxch.core.domain.model.AnnouncementsFeedSnapshot import zed.rainxch.core.domain.repository.AnnouncementsRepository import zed.rainxch.core.domain.utils.BrowserHelper +import kotlin.time.ExperimentalTime import kotlin.time.Instant class AnnouncementsViewModel( diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index a63a4c9fa..3a586510d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -2,6 +2,7 @@ package zed.rainxch.githubstore.app.di import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf +import org.koin.core.qualifier.named import org.koin.dsl.module import zed.rainxch.apps.presentation.AppsViewModel import zed.rainxch.apps.presentation.import.ExternalImportViewModel @@ -14,6 +15,7 @@ import zed.rainxch.githubstore.app.announcements.AnnouncementsViewModel import zed.rainxch.githubstore.app.onboarding.OnboardingViewModel import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel import zed.rainxch.home.presentation.HomeViewModel +import zed.rainxch.home.presentation.categorylist.CategoryListViewModel import zed.rainxch.profile.presentation.ProfileViewModel import zed.rainxch.recentlyviewed.presentation.RecentlyViewedViewModel import zed.rainxch.search.presentation.SearchViewModel @@ -34,11 +36,11 @@ val viewModelsModule = viewModel { params -> DetailsViewModel( - repositoryId = params.get(0), - ownerParam = params.get(1), - repoParam = params.get(2), - isComingFromUpdate = params.get(3), - sourceHostParam = if (params.size() > 4) params.get(4) else null, + repositoryId = params[0], + ownerParam = params[1], + repoParam = params[2], + isComingFromUpdate = params[3], + sourceHostParam = if (params.size() > 4) params[4] else null, detailsRepository = get(), downloader = get(), installer = get(), @@ -61,7 +63,6 @@ val viewModelsModule = apkInspector = get(), userSessionRepository = get(), systemInstallSerializer = get(), - profileRepository = get(), ) } viewModelOf(::DeveloperProfileViewModel) @@ -82,9 +83,9 @@ val viewModelsModule = tweaksRepository = get(), seenReposRepository = get(), searchHistoryRepository = get(), - profileRepository = get(), hiddenReposRepository = get(), - initialPlatform = params.getOrNull(), + userSessionRepository = get(), + initialPlatform = get(), ) } viewModelOf(::ProfileViewModel) @@ -99,7 +100,7 @@ val viewModelsModule = viewModelOf(::AnnouncementsViewModel) viewModelOf(::OnboardingViewModel) viewModel { params -> - zed.rainxch.home.presentation.categorylist.CategoryListViewModel( + CategoryListViewModel( category = params.get(), homeRepository = get(), ) @@ -110,8 +111,7 @@ val viewModelsModule = testHttpClient = get( qualifier = - org.koin.core.qualifier - .named("test"), + named("test"), ), ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt index 79d8b4019..402f1cb1b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt @@ -14,7 +14,6 @@ import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.details.data.di.detailsModule import zed.rainxch.devprofile.data.di.devProfileModule import zed.rainxch.home.data.di.homeModule -import zed.rainxch.profile.data.di.profileModule import zed.rainxch.search.data.di.searchModule fun initKoin(config: KoinAppDeclaration? = null) { @@ -35,7 +34,6 @@ fun initKoin(config: KoinAppDeclaration? = null) { devProfileModule, homeModule, searchModule, - profileModule, ) } val koin = app.koin diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index f0b9b4a6a..f7f6ce441 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -104,7 +104,7 @@ val coreModule = UserSessionRepositoryImpl( tokenStore = get(), cacheManager = get(), - clientProvider = get(), + httpClientProvider = { get().client }, logger = get(), ) } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt index d8c54d1d0..fe495799a 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt @@ -36,16 +36,6 @@ object ProxyManager { fun configFlow(scope: ProxyScope): StateFlow = flows.getValue(scope).asStateFlow() - /** - * Idempotent async seed. First call kicks off a background read of every - * [ProxyScope]'s persisted config and pushes it into the flows. Subsequent - * calls no-op (cheap volatile check + Mutex). Flows start at - * [ProxyConfig.System] until the seed completes — typical bootstrap is - * sub-200ms, well before any HTTP traffic. - * - * Replaces the previous `runBlocking` factory in DI. Call once from - * `initKoin()` after `startKoin` returns, fire-and-forget. - */ fun bootstrap(repository: ProxyRepository, appScope: CoroutineScope) { if (seedJob != null) return appScope.launch { diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt index 836976515..42f83e077 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt @@ -25,8 +25,8 @@ import zed.rainxch.core.domain.model.GithubAsset import zed.rainxch.core.domain.model.GithubRelease import zed.rainxch.core.domain.model.InstallSource import zed.rainxch.core.domain.model.InstalledApp +import zed.rainxch.core.domain.model.MatchingPreview import zed.rainxch.core.domain.repository.InstalledAppsRepository -import zed.rainxch.core.domain.repository.MatchingPreview import zed.rainxch.core.domain.system.Installer import zed.rainxch.core.domain.model.isEffectivelyPreRelease import zed.rainxch.core.domain.util.AssetFilter diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt index 02ff6e7c4..24e776cbf 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/UserSessionRepositoryImpl.kt @@ -20,7 +20,6 @@ import zed.rainxch.core.data.cache.CacheManager.CacheTtl.USER_PROFILE import zed.rainxch.core.data.data_source.TokenStore import zed.rainxch.core.data.dto.UserProfileNetwork import zed.rainxch.core.data.mappers.toUserProfile -import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.executeRequest import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.UserProfile @@ -32,10 +31,10 @@ import kotlin.time.ExperimentalTime class UserSessionRepositoryImpl( private val tokenStore: TokenStore, private val cacheManager: CacheManager, - private val clientProvider: GitHubClientProvider, + private val httpClientProvider: () -> HttpClient, private val logger: GitHubStoreLogger ) : UserSessionRepository { - private val httpClient: HttpClient get() = clientProvider.client + private val httpClient: HttpClient get() = httpClientProvider() private val _sessionExpiredEvent = MutableSharedFlow(extraBufferCapacity = 1) override val sessionExpiredEvent: SharedFlow = _sessionExpiredEvent.asSharedFlow() diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 121db5e48..7052826d3 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -1,65 +1,50 @@ package zed.rainxch.home.presentation -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -import zed.rainxch.core.domain.model.DiscoveryPlatform -import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.buttons.OutlineButton import zed.rainxch.core.presentation.components.section.SectionHeader import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled -import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents -import zed.rainxch.core.presentation.utils.toIcons -import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_finding_repositories +import zed.rainxch.githubstore.core.presentation.res.home_retry import zed.rainxch.home.domain.model.HomeCategory import zed.rainxch.home.presentation.components.HomeTopBar import zed.rainxch.home.presentation.components.HotCardItem import zed.rainxch.home.presentation.components.LeadCard -import zed.rainxch.home.presentation.components.PlatformFilterMenu import zed.rainxch.home.presentation.components.PopularRowItem import zed.rainxch.home.presentation.components.RepositoryActionsSheet import zed.rainxch.home.presentation.components.StarredRowItem @@ -78,56 +63,48 @@ fun HomeRoot( ) { val state by viewModel.state.collectAsStateWithLifecycle() val listState = rememberLazyListState() - val scope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() val snackbarHost = remember { SnackbarHostState() } ObserveAsEvents(viewModel.events) { event -> when (event) { - HomeEvent.OnScrollToListTop -> { - scope.launch { listState.animateScrollToItem(0) } - } - is HomeEvent.OnMessage -> { - scope.launch { snackbarHost.showSnackbar(event.message) } - } + HomeEvent.OnScrollToListTop -> coroutineScope.launch { listState.animateScrollToItem(0) } + is HomeEvent.OnMessage -> coroutineScope.launch { snackbarHost.showSnackbar(event.message) } } } HomeScreen( state = state, snackbarHost = snackbarHost, - listState = listState, onAction = { action -> when (action) { HomeAction.OnSearchClick -> onNavigateToSearch() HomeAction.OnSettingsClick -> onNavigateToSettings() HomeAction.OnAppsClick -> onNavigateToApps() - HomeAction.OnSeeAllHot -> onNavigateToCategoryList( - HomeCategory.HOT_RELEASE, - ) - HomeAction.OnSeeAllTrending -> onNavigateToCategoryList( - HomeCategory.TRENDING, - ) - HomeAction.OnSeeAllPopular -> onNavigateToCategoryList( - HomeCategory.MOST_POPULAR, - ) + HomeAction.OnSeeAllHot -> onNavigateToCategoryList(HomeCategory.HOT_RELEASE) + HomeAction.OnSeeAllTrending -> onNavigateToCategoryList(HomeCategory.TRENDING) + HomeAction.OnSeeAllPopular -> onNavigateToCategoryList(HomeCategory.MOST_POPULAR) HomeAction.OnSeeAllStarred -> onNavigateToStarredRepos() is HomeAction.OnRepoClick -> onNavigateToDetails(action.repo.id) is HomeAction.OnDeveloperClick -> onNavigateToDeveloperProfile(action.username) else -> viewModel.onAction(action) } }, + viewModel = viewModel, ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun HomeScreen( +private fun HomeScreen( state: HomeState, snackbarHost: SnackbarHostState, - listState: LazyListState, onAction: (HomeAction) -> Unit, + viewModel: HomeViewModel, ) { val bottomNavHeight = LocalBottomNavigationHeight.current + val listState = rememberLazyListState() + val uriHandler = LocalUriHandler.current Scaffold( snackbarHost = { @@ -138,279 +115,189 @@ fun HomeScreen( }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> - val uriHandler = LocalUriHandler.current Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + val sectionsAreEmpty = state.lead == null && state.hot.isEmpty() && + state.trending.isEmpty() && state.popular.isEmpty() && state.starred.isEmpty() val isAnyLoading = state.isHotLoading || state.isTrendingLoading || state.isPopularLoading || state.isStarredLoading - val sectionsAreEmpty = state.hot.isEmpty() && state.trending.isEmpty() && - state.popular.isEmpty() && state.starred.isEmpty() when { - isAnyLoading && sectionsAreEmpty -> LoadingState() - state.errorMessage != null && sectionsAreEmpty -> ErrorState( - message = state.errorMessage, - onRetry = { onAction(HomeAction.OnRetry) }, - ) - else -> FeedContent( - state = state, + isAnyLoading && sectionsAreEmpty -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(Res.string.home_finding_repositories), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + state.errorMessage != null && sectionsAreEmpty -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = state.errorMessage, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + OutlineButton(onClick = { onAction(HomeAction.OnRetry) }) { + Text(text = stringResource(Res.string.home_retry)) + } + } + } + + else -> ScrollbarContainer( listState = listState, - onAction = onAction, - ) + enabled = LocalScrollbarEnabled.current, + modifier = Modifier.fillMaxSize(), + ) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 12.dp, + end = 12.dp, + top = 0.dp, + bottom = bottomNavHeight + 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + item(key = "top_bar") { HomeTopBar() } + + state.lead?.let { lead -> + item(key = "lead_${lead.id}") { + LeadCard( + card = lead, + onClick = { onAction(HomeAction.OnRepoClick(lead.rawRepository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(lead.id)) }, + modifier = Modifier.padding(vertical = 4.dp), + ) + } + } + + if (state.hot.isNotEmpty()) { + item(key = "hot_header") { + SectionHeader( + title = "Hot releases", + subCount = state.hot.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllHot) }, + ) + } + item(key = "hot_row") { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(horizontal = 4.dp), + ) { + items(items = state.hot, key = { "hot_${it.id}" }) { card -> + HotCardItem( + card = card, + onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + ) + } + } + } + } + + if (state.trending.isNotEmpty()) { + item(key = "trending_header") { + SectionHeader( + title = "Trending now", + subCount = state.trending.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllTrending) }, + ) + } + itemsIndexed( + items = state.trending, + key = { _, card -> "trending_${card.id}" }, + ) { index, card -> + TrendingRowItem( + card = card, + rank = index + 1, + onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + ) + } + } + + if (state.popular.isNotEmpty()) { + item(key = "popular_header") { + SectionHeader( + title = "Most popular", + subCount = state.popular.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllPopular) }, + ) + } + itemsIndexed( + items = state.popular, + key = { _, card -> "popular_${card.id}" }, + ) { index, card -> + PopularRowItem( + card = card, + rank = index + 1, + onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + ) + } + } + + if (state.isUserSignedIn && state.starred.isNotEmpty()) { + item(key = "starred_header") { + SectionHeader( + title = "From your stars", + subCount = state.starred.size.toString(), + onSeeAll = { onAction(HomeAction.OnSeeAllStarred) }, + ) + } + items(items = state.starred, key = { "starred_${it.id}" }) { card -> + StarredRowItem( + card = card, + onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, + onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + ) + } + } + } + } } - val actionRepo = state.actionSheetRepoId - ?.let { findRepo(state, it) } - if (actionRepo != null) { + state.actionSheetCard?.let { card -> RepositoryActionsSheet( - repository = actionRepo.repository, - isSeen = actionRepo.isSeen, + repository = card.rawRepository, + isSeen = card.isSeen, onDismiss = { onAction(HomeAction.OnActionSheetDismiss) }, onShare = { onAction(HomeAction.OnActionSheetDismiss) - onAction(HomeAction.OnShareClick(actionRepo.repository)) + onAction(HomeAction.OnShareClick(card.rawRepository)) }, onOpenOnGithub = { onAction(HomeAction.OnActionSheetDismiss) - uriHandler.openUri(actionRepo.repository.htmlUrl) + uriHandler.openUri(card.rawRepository.htmlUrl) }, onToggleSeen = { onAction(HomeAction.OnActionSheetDismiss) - if (actionRepo.isSeen) { - onAction(HomeAction.OnMarkAsUnseen(actionRepo.repository.id)) + if (card.isSeen) { + onAction(HomeAction.OnMarkAsUnseen(card.id)) } else { - onAction(HomeAction.OnMarkAsSeen(actionRepo.repository)) + onAction(HomeAction.OnMarkAsSeen(card.rawRepository)) } }, onHide = { onAction(HomeAction.OnActionSheetDismiss) - onAction(HomeAction.OnHideRepository(actionRepo.repository)) + onAction(HomeAction.OnHideRepository(card.rawRepository)) }, ) } } } } - -@Composable -private fun FeedContent( - state: HomeState, - listState: LazyListState, - onAction: (HomeAction) -> Unit, -) { - val bottomNavHeight = LocalBottomNavigationHeight.current - val visibleHot by visibleReposState(state, state.hot) - val visibleTrending by visibleReposState(state, state.trending) - val visiblePopular by visibleReposState(state, state.popular) - val visibleStarred by visibleReposState(state, state.starred) - - val lead = visibleHot.firstOrNull() - val homeLimit = 6 - val hotTail = visibleHot.drop(1).take(homeLimit) - val trendingPreview = visibleTrending.take(homeLimit) - val popularPreview = visiblePopular.take(homeLimit) - val starredPreview = visibleStarred.take(5) - - val isScrollbarEnabled = LocalScrollbarEnabled.current - ScrollbarContainer( - listState = listState, - enabled = isScrollbarEnabled, - modifier = Modifier.fillMaxSize(), - ) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues( - start = 12.dp, - end = 12.dp, - top = 0.dp, - bottom = bottomNavHeight + 32.dp, - ), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - item(key = "top_bar") { - HomeTopBar() - } - - if (lead != null) { - item(key = "lead_${lead.repository.id}") { - LeadCard( - repo = lead, - onClick = { onAction(HomeAction.OnRepoClick(lead.repository)) }, - onLongClick = { onAction(HomeAction.OnRepoLongClick(lead.repository.id)) }, - modifier = Modifier.padding(vertical = 4.dp), - ) - } - } - - if (hotTail.isNotEmpty()) { - item(key = "hot_header") { - SectionHeader( - title = "Hot releases", - subCount = hotTail.size.toString(), - onSeeAll = { onAction(HomeAction.OnSeeAllHot) }, - ) - } - item(key = "hot_row") { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(10.dp), - contentPadding = PaddingValues(horizontal = 4.dp), - ) { - items(items = hotTail, key = { "hot_${it.repository.id}" }) { repo -> - HotCardItem( - repo = repo, - onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, - onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, - ) - } - } - } - } - - if (trendingPreview.isNotEmpty()) { - item(key = "trending_header") { - SectionHeader( - title = "Trending now", - subCount = trendingPreview.size.toString(), - onSeeAll = { onAction(HomeAction.OnSeeAllTrending) }, - ) - } - itemsIndexed( - items = trendingPreview, - key = { _, repo -> "trending_${repo.repository.id}" }, - ) { idx, repo -> - TrendingRowItem( - repo = repo, - rank = idx + 1, - onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, - onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, - ) - } - } - - if (popularPreview.isNotEmpty()) { - item(key = "popular_header") { - SectionHeader( - title = "Most popular", - subCount = popularPreview.size.toString(), - onSeeAll = { onAction(HomeAction.OnSeeAllPopular) }, - ) - } - itemsIndexed( - items = popularPreview, - key = { _, repo -> "popular_${repo.repository.id}" }, - ) { idx, repo -> - PopularRowItem( - repo = repo, - rank = idx + 1, - onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, - onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, - ) - } - } - - if (state.isUserSignedIn && starredPreview.isNotEmpty()) { - item(key = "starred_header") { - SectionHeader( - title = "From your stars", - subCount = starredPreview.size.toString(), - onSeeAll = { onAction(HomeAction.OnSeeAllStarred) }, - ) - } - items(items = starredPreview, key = { "starred_${it.repository.id}" }) { repo -> - StarredRowItem( - repo = repo, - onClick = { onAction(HomeAction.OnRepoClick(repo.repository)) }, - onLongClick = { onAction(HomeAction.OnRepoLongClick(repo.repository.id)) }, - ) - } - } - } - } -} - -@Composable -private fun visibleReposState( - state: HomeState, - source: kotlinx.collections.immutable.ImmutableList, -) = remember(source, state.hiddenRepoIds, state.seenRepoIds, state.isHideSeenEnabled) { - derivedStateOf { - val hidden = state.hiddenRepoIds - val needsHideSeen = state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty() - if (hidden.isEmpty() && !needsHideSeen) { - source - } else { - source.filter { repo -> - repo.repository.id !in hidden && - (!needsHideSeen || repo.repository.id !in state.seenRepoIds) - } - } - } -} - -private fun findRepo( - state: HomeState, - repoId: Long, -): zed.rainxch.core.presentation.model.DiscoveryRepositoryUi? { - fun seq() = sequence { - yieldAll(state.hot) - yieldAll(state.trending) - yieldAll(state.popular) - yieldAll(state.starred) - } - return seq().firstOrNull { it.repository.id == repoId } -} - -@Composable -private fun LoadingState() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularProgressIndicator() - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(Res.string.home_finding_repositories), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } -} - -@Composable -private fun ErrorState(message: String, onRetry: () -> Unit) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(16.dp), - ) { - Text( - text = message, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(8.dp)) - GithubStoreButton( - text = stringResource(Res.string.home_retry), - onClick = onRetry, - ) - } - } -} - -@Preview -@Composable -private fun Preview() { - GithubStoreTheme { - HomeScreen( - state = HomeState(), - snackbarHost = SnackbarHostState(), - listState = rememberLazyListState(), - onAction = {}, - ) - } -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt index 6ea5db3f1..8ecacb1c6 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt @@ -4,16 +4,15 @@ import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import zed.rainxch.core.domain.model.DiscoveryPlatform -import zed.rainxch.core.domain.model.InstalledApp -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi +import zed.rainxch.home.presentation.model.HomeRepoCardUi @Stable data class HomeState( - val hot: ImmutableList = persistentListOf(), - val trending: ImmutableList = persistentListOf(), - val popular: ImmutableList = persistentListOf(), - val starred: ImmutableList = persistentListOf(), - val installedApps: ImmutableList = persistentListOf(), + val lead: HomeRepoCardUi? = null, + val hot: ImmutableList = persistentListOf(), + val trending: ImmutableList = persistentListOf(), + val popular: ImmutableList = persistentListOf(), + val starred: ImmutableList = persistentListOf(), val isHotLoading: Boolean = false, val isTrendingLoading: Boolean = false, val isPopularLoading: Boolean = false, @@ -25,7 +24,5 @@ data class HomeState( val isUpdateAvailable: Boolean = false, val isHideSeenEnabled: Boolean = false, val isUserSignedIn: Boolean = false, - val seenRepoIds: Set = emptySet(), - val hiddenRepoIds: Set = emptySet(), - val actionSheetRepoId: Long? = null, + val actionSheetCard: HomeRepoCardUi? = null, ) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 9f7b4f665..8d2048944 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -7,13 +7,15 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn @@ -37,10 +39,45 @@ import zed.rainxch.core.domain.repository.TweaksRepository import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.core.domain.utils.ShareManager -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi -import zed.rainxch.core.presentation.utils.toUi -import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.failed_to_share_link +import zed.rainxch.githubstore.core.presentation.res.home_failed_to_load_repositories +import zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard import zed.rainxch.home.domain.repository.HomeRepository +import zed.rainxch.home.presentation.model.HomeRepoCardUi +import zed.rainxch.home.presentation.model.toHomeRepoCardUi + +private data class RawRepo( + val raw: GithubRepoSummary, + val isInstalled: Boolean, + val isUpdateAvailable: Boolean, + val isFavourite: Boolean, + val isStarred: Boolean, +) + +private data class RawHomeState( + val hot: List = emptyList(), + val trending: List = emptyList(), + val popular: List = emptyList(), + val starred: List = emptyList(), + val installedById: Map> = emptyMap(), + val favouriteIds: Set = emptySet(), + val starredIds: Set = emptySet(), + val seenIds: Set = emptySet(), + val hiddenIds: Set = emptySet(), + val isHideSeenEnabled: Boolean = false, + val isHotLoading: Boolean = false, + val isTrendingLoading: Boolean = false, + val isPopularLoading: Boolean = false, + val isStarredLoading: Boolean = false, + val errorMessage: String? = null, + val selectedPlatforms: Set = emptySet(), + val isPlatformPopupVisible: Boolean = false, + val isUpdateAvailable: Boolean = false, + val isUserSignedIn: Boolean = false, + val currentUserLogin: String? = null, + val actionSheetRepoId: Long? = null, +) class HomeViewModel( private val homeRepository: HomeRepository, @@ -55,141 +92,124 @@ class HomeViewModel( private val logger: GitHubStoreLogger, private val shareManager: ShareManager, ) : ViewModel() { + private var hasLoadedInitialData = false private var loadJob: Job? = null - @Volatile private var currentUserLogin: String? = null - - private val _state = MutableStateFlow(HomeState()) - val state = - _state - .onStart { - if (!hasLoadedInitialData) { - observeCurrentUser() - syncSystemState() - - refreshAllSections(isInitial = true) - observeInstalledApps() - observeFavourites() - observeStarredRepos() - observeSeenRepos() - observeHiddenRepos() - observeDiscoveryPlatforms() - observeHideSeenEnabled() - - hasLoadedInitialData = true - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000L), - initialValue = HomeState(), - ) + private val rawState = MutableStateFlow(RawHomeState()) + + val state: StateFlow = rawState + .onStart { + if (!hasLoadedInitialData) { + observeCurrentUser() + syncSystemState() + refreshAllSections(isInitial = true) + observeInstalledApps() + observeFavourites() + observeStarredRepos() + observeSeenRepos() + observeHiddenRepos() + observeDiscoveryPlatforms() + observeHideSeenEnabled() + hasLoadedInitialData = true + } + } + .map { it.toView() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = HomeState(), + ) private val _events = Channel(capacity = Channel.BUFFERED) val events = _events.receiveAsFlow() fun onAction(action: HomeAction) { when (action) { - HomeAction.OnRefreshClick -> { - viewModelScope.launch { - syncInstalledAppsUseCase() - refreshAllSections(isInitial = false) - } + HomeAction.OnRefreshClick -> viewModelScope.launch { + syncInstalledAppsUseCase() + refreshAllSections(isInitial = false) } HomeAction.OnRetry -> refreshAllSections(isInitial = true) - HomeAction.OnPlatformPopupOpen -> { - _state.update { it.copy(isPlatformPopupVisible = true) } - } + HomeAction.OnPlatformPopupOpen -> + rawState.update { it.copy(isPlatformPopupVisible = true) } - HomeAction.OnPlatformPopupDismiss -> { - _state.update { it.copy(isPlatformPopupVisible = false) } - } + HomeAction.OnPlatformPopupDismiss -> + rawState.update { it.copy(isPlatformPopupVisible = false) } - is HomeAction.OnRepoLongClick -> { - _state.update { it.copy(actionSheetRepoId = action.repoId) } - } + is HomeAction.OnRepoLongClick -> + rawState.update { it.copy(actionSheetRepoId = action.repoId) } - HomeAction.OnActionSheetDismiss -> { - _state.update { it.copy(actionSheetRepoId = null) } - } + HomeAction.OnActionSheetDismiss -> + rawState.update { it.copy(actionSheetRepoId = null) } - is HomeAction.OnShareClick -> { - viewModelScope.launch { - runCatching { - shareManager.shareText("https://github-store.org/app?repo=${action.repo.fullName}") - }.onFailure { t -> - logger.error("Failed to share link: ${t.message}") - _events.send(HomeEvent.OnMessage(getString(Res.string.failed_to_share_link))) - return@launch - } - if (getPlatform() != Platform.ANDROID) { - _events.send(HomeEvent.OnMessage(getString(Res.string.link_copied_to_clipboard))) - } + is HomeAction.OnShareClick -> viewModelScope.launch { + runCatching { + shareManager.shareText("https://github-store.org/app?repo=${action.repo.fullName}") + }.onFailure { t -> + logger.error("Failed to share link: ${t.message}") + _events.send(HomeEvent.OnMessage(getString(Res.string.failed_to_share_link))) + return@launch + } + if (getPlatform() != Platform.ANDROID) { + _events.send(HomeEvent.OnMessage(getString(Res.string.link_copied_to_clipboard))) } } - is HomeAction.OnHideRepository -> { + is HomeAction.OnHideRepository -> viewModelScope.launch { val repo = action.repo - viewModelScope.launch { - try { - hiddenReposRepository.hide( - repoId = repo.id, - repoName = repo.name, - repoOwner = repo.owner.login, - repoOwnerAvatarUrl = repo.owner.avatarUrl, - ) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.warn("Hide repository failed for ${repo.id}: ${e.message}") - } + try { + hiddenReposRepository.hide( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Hide repository failed for ${repo.id}: ${e.message}") } } - is HomeAction.OnUndoHideRepository -> { - viewModelScope.launch { - try { - hiddenReposRepository.unhide(action.repoId) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.warn("Unhide repository failed for ${action.repoId}: ${e.message}") - } + is HomeAction.OnUndoHideRepository -> viewModelScope.launch { + try { + hiddenReposRepository.unhide(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Unhide repository failed for ${action.repoId}: ${e.message}") } } - is HomeAction.OnMarkAsSeen -> { + is HomeAction.OnMarkAsSeen -> viewModelScope.launch { val repo = action.repo - viewModelScope.launch { - try { - seenReposRepository.markAsSeen( - repoId = repo.id, - repoName = repo.name, - repoOwner = repo.owner.login, - repoOwnerAvatarUrl = repo.owner.avatarUrl, - repoDescription = repo.description, - primaryLanguage = repo.language, - repoUrl = repo.htmlUrl, - ) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.warn("Mark as seen failed for ${repo.id}: ${e.message}") - } + try { + seenReposRepository.markAsSeen( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + repoDescription = repo.description, + primaryLanguage = repo.language, + repoUrl = repo.htmlUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as seen failed for ${repo.id}: ${e.message}") } } - is HomeAction.OnMarkAsUnseen -> { - viewModelScope.launch { - try { - seenReposRepository.removeFromHistory(action.repoId) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.warn("Mark as unseen failed for ${action.repoId}: ${e.message}") - } + is HomeAction.OnMarkAsUnseen -> viewModelScope.launch { + try { + seenReposRepository.removeFromHistory(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as unseen failed for ${action.repoId}: ${e.message}") } } @@ -207,48 +227,41 @@ class HomeViewModel( private fun refreshAllSections(isInitial: Boolean) { loadJob?.cancel() - loadJob = - viewModelScope.launch { - val platforms = tweaksRepository.getDiscoveryPlatforms().first() - _state.update { - it.copy( - selectedPlatforms = platforms, - isHotLoading = true, - isTrendingLoading = true, - isPopularLoading = true, - isStarredLoading = _state.value.isUserSignedIn, - errorMessage = null, - hot = if (isInitial) persistentListOf() else _state.value.hot, - trending = if (isInitial) persistentListOf() else _state.value.trending, - popular = if (isInitial) persistentListOf() else _state.value.popular, - starred = if (isInitial) persistentListOf() else _state.value.starred, - ) - } - - coroutineScope { - launch { loadHot(platforms) } - launch { loadTrending(platforms) } - launch { loadPopular(platforms) } - launch { loadStarred() } - } + loadJob = viewModelScope.launch { + val platforms = tweaksRepository.getDiscoveryPlatforms().first() + rawState.update { + it.copy( + selectedPlatforms = platforms, + isHotLoading = true, + isTrendingLoading = true, + isPopularLoading = true, + isStarredLoading = it.isUserSignedIn, + errorMessage = null, + hot = if (isInitial) emptyList() else it.hot, + trending = if (isInitial) emptyList() else it.trending, + popular = if (isInitial) emptyList() else it.popular, + starred = if (isInitial) emptyList() else it.starred, + ) } + coroutineScope { + launch { loadHot(platforms) } + launch { loadTrending(platforms) } + launch { loadPopular(platforms) } + launch { loadStarred() } + } + } } private suspend fun loadHot(platforms: Set) { try { val page = homeRepository.getHotReleaseRepositories(platforms, page = 1).first() - val mapped = mapReposToUi(page.repos) - _state.update { - it.copy( - hot = mapped.toImmutableList(), - isHotLoading = false, - ) - } + val mapped = wrapAll(page.repos) + rawState.update { it.copy(hot = mapped, isHotLoading = false) } } catch (t: CancellationException) { throw t } catch (t: Throwable) { logger.error("Hot section load failed: ${t.message}") - _state.update { + rawState.update { it.copy( isHotLoading = false, errorMessage = it.errorMessage ?: t.message @@ -261,42 +274,30 @@ class HomeViewModel( private suspend fun loadTrending(platforms: Set) { try { val page = homeRepository.getTrendingRepositories(platforms, page = 1).first() - val mapped = mapReposToUi(page.repos) - _state.update { - it.copy( - trending = mapped.toImmutableList(), - isTrendingLoading = false, - ) - } + rawState.update { it.copy(trending = wrapAll(page.repos), isTrendingLoading = false) } } catch (t: CancellationException) { throw t } catch (t: Throwable) { logger.error("Trending section load failed: ${t.message}") - _state.update { it.copy(isTrendingLoading = false) } + rawState.update { it.copy(isTrendingLoading = false) } } } private suspend fun loadPopular(platforms: Set) { try { val page = homeRepository.getMostPopular(platforms, page = 1).first() - val mapped = mapReposToUi(page.repos) - _state.update { - it.copy( - popular = mapped.toImmutableList(), - isPopularLoading = false, - ) - } + rawState.update { it.copy(popular = wrapAll(page.repos), isPopularLoading = false) } } catch (t: CancellationException) { throw t } catch (t: Throwable) { logger.error("Popular section load failed: ${t.message}") - _state.update { it.copy(isPopularLoading = false) } + rawState.update { it.copy(isPopularLoading = false) } } } private suspend fun loadStarred() { - if (!_state.value.isUserSignedIn) { - _state.update { it.copy(starred = persistentListOf(), isStarredLoading = false) } + if (!rawState.value.isUserSignedIn) { + rawState.update { it.copy(starred = emptyList(), isStarredLoading = false) } return } try { @@ -307,29 +308,40 @@ class HomeViewModel( .sortedByDescending { it.stargazersCount } .take(5) .map { it.repoId } - val fetched = coroutineScope { - topIds - .map { id -> - async { - runCatching { homeRepository.getRepositoryById(id) }.getOrNull() - } - } - .awaitAll() - .filterNotNull() - } - val mapped = mapReposToUi(fetched) - _state.update { - it.copy( - starred = mapped.toImmutableList(), - isStarredLoading = false, - ) + topIds.map { id -> + async { runCatching { homeRepository.getRepositoryById(id) }.getOrNull() } + }.awaitAll().filterNotNull() } + rawState.update { it.copy(starred = wrapAll(fetched), isStarredLoading = false) } } catch (t: CancellationException) { throw t } catch (t: Throwable) { logger.error("Starred section load failed: ${t.message}") - _state.update { it.copy(isStarredLoading = false) } + rawState.update { it.copy(isStarredLoading = false) } + } + } + + private suspend fun wrapAll(repos: List): List { + val installed = installedAppsRepository.getAllInstalledApps().first().groupBy { it.repoId } + val favourites = favouritesRepository.getAllFavorites().first().map { it.repoId }.toSet() + val starred = starredRepository.getAllStarred().first().map { it.repoId }.toSet() + rawState.update { + it.copy( + installedById = installed, + favouriteIds = favourites, + starredIds = starred, + ) + } + return repos.map { repo -> + val apps = installed[repo.id].orEmpty() + RawRepo( + raw = repo, + isInstalled = apps.any { it.isReallyInstalled() }, + isUpdateAvailable = apps.any { it.hasActualUpdate() }, + isFavourite = repo.id in favourites, + isStarred = repo.id in starred, + ) } } @@ -348,15 +360,16 @@ class HomeViewModel( private fun observeInstalledApps() { viewModelScope.launch { - installedAppsRepository.getAllInstalledApps().collect { installedApps -> - val installedMap = installedApps.groupBy { it.repoId } - _state.update { current -> - current.copy( - hot = current.hot.restamp(installedMap), - trending = current.trending.restamp(installedMap), - popular = current.popular.restamp(installedMap), - starred = current.starred.restamp(installedMap), - isUpdateAvailable = installedMap.values.flatten().any { it.hasActualUpdate() }, + installedAppsRepository.getAllInstalledApps().collect { apps -> + val byRepo = apps.groupBy { it.repoId } + rawState.update { snapshot -> + snapshot.copy( + installedById = byRepo, + isUpdateAvailable = apps.any { it.hasActualUpdate() }, + hot = snapshot.hot.restampInstall(byRepo), + trending = snapshot.trending.restampInstall(byRepo), + popular = snapshot.popular.restampInstall(byRepo), + starred = snapshot.starred.restampInstall(byRepo), ) } } @@ -366,7 +379,7 @@ class HomeViewModel( private fun observeDiscoveryPlatforms() { viewModelScope.launch { tweaksRepository.getDiscoveryPlatforms().collect { platforms -> - _state.update { it.copy(selectedPlatforms = platforms) } + rawState.update { it.copy(selectedPlatforms = platforms) } } } } @@ -374,15 +387,7 @@ class HomeViewModel( private fun observeSeenRepos() { viewModelScope.launch { seenReposRepository.getAllSeenRepoIds().collect { ids -> - _state.update { current -> - current.copy( - seenRepoIds = ids, - hot = current.hot.restampSeen(ids), - trending = current.trending.restampSeen(ids), - popular = current.popular.restampSeen(ids), - starred = current.starred.restampSeen(ids), - ) - } + rawState.update { it.copy(seenIds = ids) } } } } @@ -390,7 +395,7 @@ class HomeViewModel( private fun observeHiddenRepos() { viewModelScope.launch { hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> - _state.update { it.copy(hiddenRepoIds = ids) } + rawState.update { it.copy(hiddenIds = ids) } } } } @@ -398,7 +403,7 @@ class HomeViewModel( private fun observeHideSeenEnabled() { viewModelScope.launch { tweaksRepository.getHideSeenEnabled().collect { enabled -> - _state.update { it.copy(isHideSeenEnabled = enabled) } + rawState.update { it.copy(isHideSeenEnabled = enabled) } } } } @@ -406,22 +411,17 @@ class HomeViewModel( private fun observeCurrentUser() { viewModelScope.launch { userSessionRepository.getUser().collect { user -> - currentUserLogin = user?.username val signedIn = user != null - val previouslySignedIn = _state.value.isUserSignedIn - val login = user?.username - _state.update { current -> - current.copy( + val previouslySignedIn = rawState.value.isUserSignedIn + rawState.update { + it.copy( isUserSignedIn = signedIn, - hot = current.hot.restampOwner(login), - trending = current.trending.restampOwner(login), - popular = current.popular.restampOwner(login), - starred = current.starred.restampOwner(login), + currentUserLogin = user?.username, ) } if (signedIn != previouslySignedIn) { - if (signedIn) loadStarred() else _state.update { - it.copy(starred = persistentListOf(), isStarredLoading = false) + if (signedIn) loadStarred() else rawState.update { + it.copy(starred = emptyList(), isStarredLoading = false) } } } @@ -431,13 +431,14 @@ class HomeViewModel( private fun observeFavourites() { viewModelScope.launch { favouritesRepository.getAllFavorites().collect { favourites -> - val keys = favourites.map { it.repoId }.toSet() - _state.update { current -> - current.copy( - hot = current.hot.restampFavourite(keys), - trending = current.trending.restampFavourite(keys), - popular = current.popular.restampFavourite(keys), - starred = current.starred.restampFavourite(keys), + val ids = favourites.map { it.repoId }.toSet() + rawState.update { snapshot -> + snapshot.copy( + favouriteIds = ids, + hot = snapshot.hot.restampFavourite(ids), + trending = snapshot.trending.restampFavourite(ids), + popular = snapshot.popular.restampFavourite(ids), + starred = snapshot.starred.restampFavourite(ids), ) } } @@ -447,82 +448,92 @@ class HomeViewModel( private fun observeStarredRepos() { viewModelScope.launch { starredRepository.getAllStarred().collect { starredRepos -> - val keys = starredRepos.map { it.repoId }.toSet() - _state.update { current -> - current.copy( - hot = current.hot.restampStarred(keys), - trending = current.trending.restampStarred(keys), - popular = current.popular.restampStarred(keys), - starred = current.starred.restampStarred(keys), + val ids = starredRepos.map { it.repoId }.toSet() + rawState.update { snapshot -> + snapshot.copy( + starredIds = ids, + hot = snapshot.hot.restampStarred(ids), + trending = snapshot.trending.restampStarred(ids), + popular = snapshot.popular.restampStarred(ids), + starred = snapshot.starred.restampStarred(ids), ) } } } } - private suspend fun mapReposToUi(repos: List): List { - val installedAppsMap = - installedAppsRepository.getAllInstalledApps().first().groupBy { it.repoId } - val favoritesMap = - favouritesRepository.getAllFavorites().first().associateBy { it.repoId } - val starredReposMap = - starredRepository.getAllStarred().first().associateBy { it.repoId } - val seenIds = _state.value.seenRepoIds - val currentLogin = currentUserLogin - - return repos.map { repo -> - val apps = installedAppsMap[repo.id].orEmpty() - val favourite = favoritesMap[repo.id] - val starred = starredReposMap[repo.id] - - DiscoveryRepositoryUi( - isInstalled = apps.any { it.isReallyInstalled() }, - isFavourite = favourite != null, - isStarred = starred != null, - isSeen = repo.id in seenIds, - isCurrentUserOwner = - currentLogin != null && - repo.owner.login.equals(currentLogin, ignoreCase = true), - isUpdateAvailable = apps.any { it.hasActualUpdate() }, - repository = repo.toUi(), - ) - } - } - override fun onCleared() { super.onCleared() loadJob?.cancel() } } -private fun ImmutableList.restamp( - installedMap: Map>, -) = map { repo -> - val apps = installedMap[repo.repository.id].orEmpty() - repo.copy( +private fun List.restampInstall(byRepo: Map>) = map { item -> + val apps = byRepo[item.raw.id].orEmpty() + item.copy( isInstalled = apps.any { it.isReallyInstalled() }, isUpdateAvailable = apps.any { it.hasActualUpdate() }, ) -}.toImmutableList() - -private fun ImmutableList.restampSeen( - ids: Set, -) = map { it.copy(isSeen = it.repository.id in ids) }.toImmutableList() - -private fun ImmutableList.restampFavourite( - ids: Set, -) = map { it.copy(isFavourite = it.repository.id in ids) }.toImmutableList() - -private fun ImmutableList.restampStarred( - ids: Set, -) = map { it.copy(isStarred = it.repository.id in ids) }.toImmutableList() - -private fun ImmutableList.restampOwner( - login: String?, -) = map { repo -> - repo.copy( - isCurrentUserOwner = - login != null && repo.repository.owner.login.equals(login, ignoreCase = true), +} + +private fun List.restampFavourite(ids: Set) = map { + it.copy(isFavourite = it.raw.id in ids) +} + +private fun List.restampStarred(ids: Set) = map { + it.copy(isStarred = it.raw.id in ids) +} + +private fun RawHomeState.toView(): HomeState { + val hotVisible = hot.filterVisible(hiddenIds, seenIds, isHideSeenEnabled) + val leadRaw = hotVisible.firstOrNull() + val leadCard = leadRaw?.toCard(currentUserLogin, seenIds) + val hotCards = hotVisible.drop(1).take(6).map { it.toCard(currentUserLogin, seenIds) }.toImmutableList() + val trendingCards = trending.filterVisible(hiddenIds, seenIds, isHideSeenEnabled).take(6) + .map { it.toCard(currentUserLogin, seenIds) }.toImmutableList() + val popularCards = popular.filterVisible(hiddenIds, seenIds, isHideSeenEnabled).take(6) + .map { it.toCard(currentUserLogin, seenIds) }.toImmutableList() + val starredCards = starred.filterVisible(hiddenIds, seenIds, isHideSeenEnabled).take(5) + .map { it.toCard(currentUserLogin, seenIds) }.toImmutableList() + val actionSheetCard = actionSheetRepoId?.let { id -> + (hot + trending + popular + starred).firstOrNull { it.raw.id == id }?.toCard(currentUserLogin, seenIds) + } + return HomeState( + lead = leadCard, + hot = hotCards, + trending = trendingCards, + popular = popularCards, + starred = starredCards, + isHotLoading = isHotLoading, + isTrendingLoading = isTrendingLoading, + isPopularLoading = isPopularLoading, + isStarredLoading = isStarredLoading, + errorMessage = errorMessage, + selectedPlatforms = selectedPlatforms, + isPlatformPopupVisible = isPlatformPopupVisible, + isUpdateAvailable = isUpdateAvailable, + isHideSeenEnabled = isHideSeenEnabled, + isUserSignedIn = isUserSignedIn, + actionSheetCard = actionSheetCard, ) -}.toImmutableList() +} + +private fun List.filterVisible( + hiddenIds: Set, + seenIds: Set, + hideSeenEnabled: Boolean, +): List = filter { item -> + item.raw.id !in hiddenIds && (!hideSeenEnabled || item.raw.id !in seenIds) +} +private fun RawRepo.toCard(currentUserLogin: String?, seenIds: Set): HomeRepoCardUi = + toHomeRepoCardUi( + repo = raw, + isInstalled = isInstalled, + isUpdateAvailable = isUpdateAvailable, + isFavourite = isFavourite, + isStarred = isStarred, + isSeen = raw.id in seenIds, + isCurrentUserOwner = currentUserLogin != null && + raw.owner.login.equals(currentUserLogin, ignoreCase = true), + ) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt index 975f82d60..9f797120d 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt @@ -88,7 +88,7 @@ fun CategoryListScreen( .systemBarsPadding(), ) { CategoryListTopBar(state.category, onBack) - if (state.isLoading && state.repos.isEmpty()) { + if (state.isLoading && state.cards.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } @@ -103,19 +103,19 @@ fun CategoryListScreen( ), verticalArrangement = Arrangement.spacedBy(10.dp), ) { - items(items = state.repos, key = { it.repository.id }) { repo -> - val rank = state.repos.indexOf(repo) + 1 + items(items = state.cards, key = { it.id }) { card -> + val rank = state.cards.indexOf(card) + 1 when (state.category) { HomeCategory.MOST_POPULAR -> PopularRowItem( rank = rank, - repo = repo, - onClick = { onAction(CategoryListAction.OnRepoClick(repo.repository.id)) }, + card = card, + onClick = { onAction(CategoryListAction.OnRepoClick(card.id)) }, onLongClick = { }, ) else -> TrendingRowItem( rank = rank, - repo = repo, - onClick = { onAction(CategoryListAction.OnRepoClick(repo.repository.id)) }, + card = card, + onClick = { onAction(CategoryListAction.OnRepoClick(card.id)) }, onLongClick = { }, ) } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt index 2967c6a71..14642bead 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListState.kt @@ -2,12 +2,12 @@ package zed.rainxch.home.presentation.categorylist import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.home.domain.model.HomeCategory +import zed.rainxch.home.presentation.model.HomeRepoCardUi data class CategoryListState( val category: HomeCategory = HomeCategory.HOT_RELEASE, - val repos: ImmutableList = persistentListOf(), + val cards: ImmutableList = persistentListOf(), val isLoading: Boolean = false, val isLoadingMore: Boolean = false, val hasMorePages: Boolean = true, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt index 473ed8303..9a5fc3497 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListViewModel.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import zed.rainxch.core.presentation.utils.toUi import zed.rainxch.home.domain.model.HomeCategory import zed.rainxch.home.domain.repository.HomeRepository +import zed.rainxch.home.presentation.model.toHomeRepoCardUi class CategoryListViewModel( private val category: HomeCategory, @@ -40,7 +40,7 @@ class CategoryListViewModel( nextPage = 1 _state.update { it.copy( - repos = persistentListOf(), + cards = persistentListOf(), hasMorePages = true, errorMessage = null, ) @@ -76,24 +76,24 @@ class CategoryListViewModel( } return@onSuccess } - val existing: List = _state.value.repos + val existing = _state.value.cards val incoming = paginated.repos.map { repo -> - zed.rainxch.core.presentation.model.DiscoveryRepositoryUi( + toHomeRepoCardUi( + repo = repo, isInstalled = false, isUpdateAvailable = false, isFavourite = false, isStarred = false, isSeen = false, isCurrentUserOwner = false, - repository = repo.toUi(), ) } - val seenIds = existing.map { it.repository.id }.toHashSet() - val merged = (existing + incoming.filter { it.repository.id !in seenIds }).toImmutableList() + val existingIds = existing.map { it.id }.toHashSet() + val merged = (existing + incoming.filter { it.id !in existingIds }).toImmutableList() nextPage += 1 _state.update { it.copy( - repos = merged, + cards = merged, isLoading = false, isLoadingMore = false, hasMorePages = incoming.isNotEmpty(), diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt deleted file mode 100644 index 22f84080f..000000000 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTimeHelpers.kt +++ /dev/null @@ -1,37 +0,0 @@ -package zed.rainxch.home.presentation.components - -import kotlin.time.Clock -import kotlin.time.ExperimentalTime -import kotlin.time.Instant - -@OptIn(ExperimentalTime::class) -internal fun daysSinceIso(isoInstant: String): Int { - val trimmed = isoInstant.trim() - if (trimmed.isEmpty()) return Int.MAX_VALUE - val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return Int.MAX_VALUE - val nowMs = Clock.System.now().toEpochMilliseconds() - val diffMs = nowMs - parsed.toEpochMilliseconds() - if (diffMs <= 0L) return 0 - return (diffMs / 86_400_000L).toInt() -} - -@OptIn(ExperimentalTime::class) -internal fun relativeAgo(isoInstant: String): String { - val trimmed = isoInstant.trim() - if (trimmed.isEmpty()) return "" - val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return "" - val nowMs = Clock.System.now().toEpochMilliseconds() - val diffMs = nowMs - parsed.toEpochMilliseconds() - if (diffMs <= 0L) return "now" - val minutes = diffMs / 60_000L - if (minutes < 1L) return "now" - if (minutes < 60L) return "${minutes}m" - val hours = minutes / 60L - if (hours < 24L) return "${hours}h" - val days = hours / 24L - if (days < 30L) return "${days}d" - val months = days / 30L - if (months < 12L) return "${months}mo" - val years = days / 365L - return "${years}y" -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt index 39f5c1486..84cf50dc5 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -3,7 +3,6 @@ package zed.rainxch.home.presentation.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,61 +27,49 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.cards.CompactCard -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi -import zed.rainxch.core.presentation.vocabulary.AppAccentResolver +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.vocabulary.FreshnessRing import zed.rainxch.core.presentation.vocabulary.PlatformGlyph -import zed.rainxch.core.presentation.vocabulary.PlatformKind import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.core.presentation.vocabulary.TopicGlyph -import zed.rainxch.core.presentation.vocabulary.freshnessOf +import zed.rainxch.home.presentation.model.HomeRepoCardUi @OptIn(ExperimentalFoundationApi::class) @Composable fun HotCardItem( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val r = repo.repository - val days = daysSinceIso(r.updatedAt) - val isDark = isSystemInDarkTheme() - val accent = AppAccentResolver.resolve( - backendHex = null, - topics = r.topics.orEmpty(), - primaryLanguage = r.language, - ) - val freshness = freshnessOf(days) - Box( modifier = modifier .width(260.dp) - .height(220.dp) + .height(200.dp) + .clip(Radii.card) .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { - CompactCard { + CompactCard(modifier = Modifier.height(200.dp)) { Row( verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth(), ) { FreshnessRing( - daysSinceRelease = days, + daysSinceRelease = card.daysSinceUpdate, sizeDp = 44, - color = accent.c, + color = card.accentSaturated, ) { GitHubStoreImage( - imageModel = { r.owner.avatarUrl }, + imageModel = { card.ownerAvatarUrl }, modifier = Modifier.size(44.dp).clip(CircleShape), ) } Column(modifier = Modifier.weight(1f)) { Text( - text = r.name, + text = card.name, style = MaterialTheme.typography.titleMedium.copy( fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, @@ -92,13 +79,30 @@ fun HotCardItem( overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) - StarTier(stars = r.stargazersCount) + StarTier(stars = card.starsCount) + } + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(card.freshnessColor.copy(alpha = 0.18f)) + .padding(horizontal = 8.dp, vertical = 3.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(card.freshnessColor)) + Text( + text = card.relativeAgoLabel, + color = card.freshnessColor, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + ) } - DaysAgoPill(label = relativeAgo(r.updatedAt), color = freshness.color) } Spacer(Modifier.height(8.dp)) Text( - text = r.description.orEmpty(), + text = card.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, @@ -116,47 +120,14 @@ fun HotCardItem( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth(), ) { - r.topics.orEmpty().take(2).forEach { topic -> + card.topics.take(2).forEach { topic -> TopicGlyph(topic = topic, sizeDp = 14) } Box(Modifier.weight(1f)) - listOf( - DiscoveryPlatform.Android to PlatformKind.ANDROID, - DiscoveryPlatform.Windows to PlatformKind.WINDOWS, - DiscoveryPlatform.Macos to PlatformKind.MACOS, - DiscoveryPlatform.Linux to PlatformKind.LINUX, - ).forEach { (plat, kind) -> - if (plat in r.availablePlatforms) { - PlatformGlyph(kind = kind, supported = true, sizeDp = 14) - } + card.platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = 14) } } } } - - @Suppress("UNUSED_EXPRESSION") isDark -} - -@Composable -private fun DaysAgoPill(label: String, color: androidx.compose.ui.graphics.Color) { - Row( - modifier = Modifier - .clip(RoundedCornerShape(50)) - .background(color.copy(alpha = 0.18f)) - .padding(horizontal = 8.dp, vertical = 3.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier.size(5.dp).clip(CircleShape).background(color), - ) - Text( - text = label, - color = color, - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - ), - ) - } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 6cdb7c36f..57ce8863b 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -28,53 +28,77 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.buttons.PrimaryButton -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.components.cards.LeadHeroCard -import zed.rainxch.core.presentation.vocabulary.AppAccentResolver +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.vocabulary.AppAccent import zed.rainxch.core.presentation.vocabulary.FreshnessRing import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.core.presentation.vocabulary.TopicGlyph -import zed.rainxch.core.presentation.vocabulary.freshnessOf +import zed.rainxch.home.presentation.model.HomeRepoCardUi @OptIn(ExperimentalFoundationApi::class) @Composable fun LeadCard( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val r = repo.repository val isDark = isSystemInDarkTheme() - val accent = AppAccentResolver.resolve( - backendHex = null, - topics = r.topics.orEmpty(), - primaryLanguage = r.language, - ) - val days = daysSinceIso(r.updatedAt) - - Column( - modifier = modifier - .fillMaxWidth() - .combinedClickable(onClick = onClick, onLongClick = onLongClick), - ) { - HotPill(days = days, ago = relativeAgo(r.updatedAt)) + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(card.freshnessColor.copy(alpha = 0.18f)) + .padding(horizontal = 12.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background(card.freshnessColor), + ) + Text( + text = "HOT · ${card.relativeAgoLabel} ago", + color = card.freshnessColor, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + ) + } Spacer(Modifier.height(8.dp)) - LeadHeroCard(accent = accent, isDark = isDark) { + LeadHeroCard( + accent = AppAccent( + c = card.accentSaturated, + lt = card.accentLightTint, + dtAlpha = card.accentDarkAlpha, + ), + isDark = isDark, + modifier = Modifier + .clip(WonkySquircleShape.CtaPrimary) + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth(), ) { - FreshnessRing(daysSinceRelease = days, sizeDp = 80, color = accent.c) { + FreshnessRing( + sizeDp = 80, + color = card.accentSaturated, + daysSinceRelease = card.daysSinceUpdate, + ) { GitHubStoreImage( - imageModel = { r.owner.avatarUrl }, + imageModel = { card.ownerAvatarUrl }, modifier = Modifier.size(80.dp).clip(CircleShape), ) } Column(modifier = Modifier.weight(1f)) { Text( - text = r.name, + text = card.name, style = MaterialTheme.typography.headlineSmall.copy( fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, @@ -86,7 +110,7 @@ fun LeadCard( ) Spacer(Modifier.height(2.dp)) Text( - text = r.owner.login, + text = card.ownerLogin, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -97,17 +121,17 @@ fun LeadCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - StarTier(stars = r.stargazersCount) - r.topics.orEmpty().take(3).forEach { topic -> + StarTier(stars = card.starsCount) + card.topics.take(3).forEach { topic -> TopicGlyph(topic = topic, sizeDp = 14) } } } } Spacer(Modifier.height(14.dp)) - if (!r.description.isNullOrBlank()) { + if (card.description.isNotBlank()) { Text( - text = r.description!!, + text = card.description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, @@ -120,43 +144,15 @@ fun LeadCard( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { PrimaryButton(onClick = onClick) { - Text(text = ctaLabel(repo)) + Text( + text = when { + card.isUpdateAvailable -> "Update" + card.isInstalled -> "Open" + else -> "Get" + }, + ) } } } } } - -internal fun ctaLabel(repo: DiscoveryRepositoryUi): String = when { - repo.isUpdateAvailable -> "Update" - repo.isInstalled -> "Open" - else -> "Get" -} - -@Composable -private fun HotPill(days: Int, ago: String) { - val freshness = freshnessOf(days) - Row( - modifier = Modifier - .clip(RoundedCornerShape(50)) - .background(freshness.color.copy(alpha = 0.18f)) - .padding(horizontal = 12.dp, vertical = 5.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(6.dp) - .clip(CircleShape) - .background(freshness.color), - ) - Text( - text = "HOT · $ago ago", - color = freshness.color, - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - ), - ) - } -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt deleted file mode 100644 index 1e655945f..000000000 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/PlatformFilterMenu.kt +++ /dev/null @@ -1,74 +0,0 @@ -package zed.rainxch.home.presentation.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import zed.rainxch.core.domain.model.DiscoveryPlatform -import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu -import zed.rainxch.core.presentation.utils.toLabel - -@Composable -fun PlatformFilterMenu( - expanded: Boolean, - selectedPlatforms: Set, - onDismiss: () -> Unit, - onSelectAll: () -> Unit, - onToggle: (DiscoveryPlatform) -> Unit, -) { - GhsDropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { - PlatformRow( - label = DiscoveryPlatform.All.toLabel(), - isSelected = selectedPlatforms.isEmpty(), - onClick = onSelectAll, - ) - DiscoveryPlatform.selectablePlatforms.forEach { platform -> - PlatformRow( - label = platform.toLabel(), - isSelected = platform in selectedPlatforms, - onClick = { onToggle(platform) }, - ) - } - } -} - -@Composable -private fun PlatformRow( - label: String, - isSelected: Boolean, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (isSelected) { - Icon( - imageVector = Icons.Default.Done, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp), - ) - } - Text( - text = label, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt index cd1186808..bb9c5e8c4 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt @@ -27,20 +27,19 @@ import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.buttons.OutlineButton import zed.rainxch.core.presentation.components.cards.RowCard -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.vocabulary.StarTier +import zed.rainxch.home.presentation.model.HomeRepoCardUi + +private val StarredTint = Color(0xFFC49652) @OptIn(ExperimentalFoundationApi::class) @Composable fun StarredRowItem( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val r = repo.repository - val starTint = Color(0xFFC49652) - Box( modifier = modifier .fillMaxWidth() @@ -51,17 +50,17 @@ fun StarredRowItem( modifier = Modifier .size(40.dp) .clip(CircleShape) - .background(starTint.copy(alpha = 0.18f)), + .background(StarredTint.copy(alpha = 0.18f)), contentAlignment = Alignment.Center, ) { GitHubStoreImage( - imageModel = { r.owner.avatarUrl }, + imageModel = { card.ownerAvatarUrl }, modifier = Modifier.size(36.dp).clip(CircleShape), ) } Column(modifier = Modifier.weight(1f)) { Text( - text = r.name, + text = card.name, style = MaterialTheme.typography.titleSmall.copy( fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, @@ -77,14 +76,20 @@ fun StarredRowItem( Icon( imageVector = Icons.Default.Star, contentDescription = null, - tint = starTint, + tint = StarredTint, modifier = Modifier.size(12.dp), ) - StarTier(stars = r.stargazersCount, size = 10) + StarTier(stars = card.starsCount, size = 10) } } OutlineButton(onClick = onClick) { - Text(text = ctaLabel(repo)) + Text( + text = when { + card.isUpdateAvailable -> "Update" + card.isInstalled -> "Open" + else -> "Get" + }, + ) } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt index e6e9d63da..5eb1993c2 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt @@ -22,26 +22,24 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.buttons.OutlineButton import zed.rainxch.core.presentation.components.cards.RowCard -import zed.rainxch.core.presentation.model.DiscoveryRepositoryUi import zed.rainxch.core.presentation.vocabulary.PlatformGlyph -import zed.rainxch.core.presentation.vocabulary.PlatformKind import zed.rainxch.core.presentation.vocabulary.StarTier +import zed.rainxch.home.presentation.model.HomeRepoCardUi @OptIn(ExperimentalFoundationApi::class) @Composable fun TrendingRowItem( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, rank: Int, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { RankRowItem( - repo = repo, + card = card, rank = rank, rankColor = MaterialTheme.colorScheme.primary, onClick = onClick, @@ -53,14 +51,14 @@ fun TrendingRowItem( @OptIn(ExperimentalFoundationApi::class) @Composable fun PopularRowItem( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, rank: Int, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { RankRowItem( - repo = repo, + card = card, rank = rank, rankColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f), onClick = onClick, @@ -72,15 +70,13 @@ fun PopularRowItem( @OptIn(ExperimentalFoundationApi::class) @Composable private fun RankRowItem( - repo: DiscoveryRepositoryUi, + card: HomeRepoCardUi, rank: Int, rankColor: Color, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val r = repo.repository - Box( modifier = modifier .fillMaxWidth() @@ -98,12 +94,12 @@ private fun RankRowItem( modifier = Modifier.width(38.dp), ) GitHubStoreImage( - imageModel = { r.owner.avatarUrl }, + imageModel = { card.ownerAvatarUrl }, modifier = Modifier.size(36.dp).clip(CircleShape), ) Column(modifier = Modifier.weight(1f)) { Text( - text = r.name, + text = card.name, style = MaterialTheme.typography.titleSmall.copy( fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, @@ -116,9 +112,9 @@ private fun RankRowItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - StarTier(stars = r.stargazersCount, size = 10) + StarTier(stars = card.starsCount, size = 10) Text( - text = r.owner.login, + text = card.ownerLogin, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -130,21 +126,19 @@ private fun RankRowItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - listOf( - DiscoveryPlatform.Android to PlatformKind.ANDROID, - DiscoveryPlatform.Windows to PlatformKind.WINDOWS, - DiscoveryPlatform.Macos to PlatformKind.MACOS, - DiscoveryPlatform.Linux to PlatformKind.LINUX, - ).forEach { (plat, kind) -> - if (plat in r.availablePlatforms) { - PlatformGlyph(kind = kind, supported = true, sizeDp = 12) - } + card.platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = 12) } } OutlineButton(onClick = onClick) { - Text(text = ctaLabel(repo)) + Text( + text = when { + card.isUpdateAvailable -> "Update" + card.isInstalled -> "Open" + else -> "Get" + }, + ) } } } } - diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt new file mode 100644 index 000000000..e28ee19c3 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt @@ -0,0 +1,94 @@ +package zed.rainxch.home.presentation.model + +import kotlinx.collections.immutable.toImmutableList +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import zed.rainxch.core.domain.model.DiscoveryPlatform +import zed.rainxch.core.domain.model.GithubRepoSummary +import zed.rainxch.core.presentation.utils.toUi +import zed.rainxch.core.presentation.vocabulary.AppAccentResolver +import zed.rainxch.core.presentation.vocabulary.PlatformKind +import zed.rainxch.core.presentation.vocabulary.freshnessOf + +fun toHomeRepoCardUi( + repo: GithubRepoSummary, + isInstalled: Boolean, + isUpdateAvailable: Boolean, + isFavourite: Boolean, + isStarred: Boolean, + isSeen: Boolean, + isCurrentUserOwner: Boolean, +): HomeRepoCardUi { + val ui = repo.toUi() + val days = daysSinceIso(repo.updatedAt) + val freshness = freshnessOf(days) + val accent = AppAccentResolver.resolve( + backendHex = null, + topics = repo.topics.orEmpty(), + primaryLanguage = repo.language, + ) + return HomeRepoCardUi( + id = ui.id, + name = ui.name, + ownerLogin = ui.owner.login, + ownerAvatarUrl = ui.owner.avatarUrl, + description = ui.description.orEmpty(), + starsCount = ui.stargazersCount, + daysSinceUpdate = days, + relativeAgoLabel = relativeAgo(repo.updatedAt), + freshnessState = freshness.state, + freshnessFraction = freshness.ringFraction, + freshnessColor = freshness.color, + accentSaturated = accent.c, + accentLightTint = accent.lt, + accentDarkAlpha = accent.dtAlpha, + topics = ui.topics.orEmpty().toImmutableList(), + platforms = ui.availablePlatforms.mapNotNull { platform -> + when (platform) { + DiscoveryPlatform.Android -> PlatformKind.ANDROID + DiscoveryPlatform.Windows -> PlatformKind.WINDOWS + DiscoveryPlatform.Macos -> PlatformKind.MACOS + DiscoveryPlatform.Linux -> PlatformKind.LINUX + else -> null + } + }.toImmutableList(), + isInstalled = isInstalled, + isUpdateAvailable = isUpdateAvailable, + isFavourite = isFavourite, + isStarred = isStarred, + isSeen = isSeen, + isCurrentUserOwner = isCurrentUserOwner, + rawRepository = ui, + ) +} + +@OptIn(ExperimentalTime::class) +private fun daysSinceIso(isoInstant: String): Int { + val trimmed = isoInstant.trim() + if (trimmed.isEmpty()) return Int.MAX_VALUE + val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return Int.MAX_VALUE + val diffMs = Clock.System.now().toEpochMilliseconds() - parsed.toEpochMilliseconds() + if (diffMs <= 0L) return 0 + return (diffMs / 86_400_000L).toInt() +} + +@OptIn(ExperimentalTime::class) +private fun relativeAgo(isoInstant: String): String { + val trimmed = isoInstant.trim() + if (trimmed.isEmpty()) return "" + val parsed = runCatching { Instant.parse(trimmed) }.getOrNull() ?: return "" + val diffMs = Clock.System.now().toEpochMilliseconds() - parsed.toEpochMilliseconds() + if (diffMs <= 0L) return "now" + val minutes = diffMs / 60_000L + if (minutes < 1L) return "now" + if (minutes < 60L) return "${minutes}m" + val hours = minutes / 60L + if (hours < 24L) return "${hours}h" + val days = hours / 24L + if (days < 30L) return "${days}d" + val months = days / 30L + if (months < 12L) return "${months}mo" + val years = days / 365L + return "${years}y" +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt new file mode 100644 index 000000000..41086efa4 --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt @@ -0,0 +1,35 @@ +package zed.rainxch.home.presentation.model + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import kotlinx.collections.immutable.ImmutableList +import zed.rainxch.core.presentation.model.GithubRepoSummaryUi +import zed.rainxch.core.presentation.vocabulary.FreshnessState +import zed.rainxch.core.presentation.vocabulary.PlatformKind + +@Immutable +data class HomeRepoCardUi( + val id: Long, + val name: String, + val ownerLogin: String, + val ownerAvatarUrl: String, + val description: String, + val starsCount: Int, + val daysSinceUpdate: Int, + val relativeAgoLabel: String, + val freshnessState: FreshnessState, + val freshnessFraction: Float, + val freshnessColor: Color, + val accentSaturated: Color, + val accentLightTint: Color, + val accentDarkAlpha: Float, + val topics: ImmutableList, + val platforms: ImmutableList, + val isInstalled: Boolean, + val isUpdateAvailable: Boolean, + val isFavourite: Boolean, + val isStarred: Boolean, + val isSeen: Boolean, + val isCurrentUserOwner: Boolean, + val rawRepository: GithubRepoSummaryUi, +) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 54a47b646..3c7cd7ed1 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -23,7 +22,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString @@ -33,8 +31,12 @@ import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.presentation.components.ClearDownloadsDialog +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.downloads_cleared +import zed.rainxch.githubstore.core.presentation.res.logout_success +import zed.rainxch.githubstore.core.presentation.res.profile_title +import zed.rainxch.githubstore.core.presentation.res.proxy_saved +import zed.rainxch.githubstore.core.presentation.res.seen_history_cleared import zed.rainxch.profile.presentation.components.LogoutDialog import zed.rainxch.profile.presentation.components.sections.logout import zed.rainxch.profile.presentation.components.sections.profile diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index e98e9af24..38ae17b37 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -25,9 +25,11 @@ import zed.rainxch.core.domain.model.TranslationProvider import zed.rainxch.core.domain.network.ProxyTestOutcome import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.network.ProxyTester +import zed.rainxch.core.domain.repository.CacheRepository import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.TweaksRepository +import zed.rainxch.core.domain.repository.UserSessionRepository import zed.rainxch.core.domain.system.AggressiveOemDetector import zed.rainxch.core.domain.system.AppVersionInfo import zed.rainxch.core.domain.system.InstallerStatusProvider @@ -56,6 +58,7 @@ class TweaksViewModel( private val seenReposRepository: SeenReposRepository, private val logger: GitHubStoreLogger, private val aggressiveOemDetector: AggressiveOemDetector, + private val cacheRepository: CacheRepository, ) : ViewModel() { private companion object { private const val BATTERY_OPT_PREF_READ_TIMEOUT_MS: Long = 1_000 @@ -121,7 +124,7 @@ class TweaksViewModel( if (cacheSizeJob?.isActive == true) return cacheSizeJob = viewModelScope.launch { - profileRepository.observeCacheSize().collect { sizeBytes -> + cacheRepository.observeCacheSize().collect { sizeBytes -> _state.update { it.copy(cacheSize = formatCacheSize(sizeBytes)) } @@ -832,7 +835,7 @@ class TweaksViewModel( _state.update { it.copy(isClearDownloadsDialogVisible = false) } viewModelScope.launch { runCatching { - profileRepository.clearCache() + cacheRepository.clearCache() }.onSuccess { cacheSizeJob?.cancel() cacheSizeJob = null From f632657caf44b5b29677890b1ae326b29bbbcb22 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:13 +0500 Subject: [PATCH 032/172] chore: drop unused pushed_at field --- .../kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt | 1 - .../kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt | 1 - .../zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt | 1 - .../data/mappers/GithubRepoNetworkModelToGitHubRepoResponse.kt | 1 - .../zed/rainxch/devprofile/domain/model/DeveloperRepository.kt | 1 - .../devprofile/presentation/components/DeveloperRepoItem.kt | 1 - 6 files changed, 6 deletions(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt index 789d6c079..d97d1e0e7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/GithubRepoNetworkModel.kt @@ -21,6 +21,5 @@ data class GithubRepoNetworkModel( @SerialName("fork") val fork: Boolean = false, @SerialName("archived") val archived: Boolean = false, @SerialName("open_issues_count") val openIssuesCount: Int = 0, - @SerialName("pushed_at") val pushedAt: String? = null, @SerialName("has_downloads") val hasDownloads: Boolean = false, ) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt index ff02a3513..cfc598ec6 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubRepoResponse.kt @@ -15,7 +15,6 @@ data class GitHubRepoResponse( @SerialName("open_issues_count") val openIssuesCount: Int, val language: String? = null, @SerialName("updated_at") val updatedAt: String, - @SerialName("pushed_at") val pushedAt: String? = null, @SerialName("has_downloads") val hasDownloads: Boolean = false, val archived: Boolean = false, val fork: Boolean = false, diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt index 1b6529377..e11c04d48 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt @@ -25,5 +25,4 @@ fun GitHubRepoResponse.toDomain( isFavorite = isFavorite, latestVersion = latestVersion, updatedAt = updatedAt, - pushedAt = pushedAt, ) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GithubRepoNetworkModelToGitHubRepoResponse.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GithubRepoNetworkModelToGitHubRepoResponse.kt index cd0ff9bd2..d9a8bd8b6 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GithubRepoNetworkModelToGitHubRepoResponse.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GithubRepoNetworkModelToGitHubRepoResponse.kt @@ -15,7 +15,6 @@ fun GithubRepoNetworkModel.toGitHubRepoResponse(): GitHubRepoResponse = openIssuesCount = openIssuesCount, language = language, updatedAt = updatedAt, - pushedAt = pushedAt, hasDownloads = hasDownloads, archived = archived, fork = fork, diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt index 0dbad7cdb..b610b196e 100644 --- a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt @@ -16,5 +16,4 @@ data class DeveloperRepository( val isFavorite: Boolean = false, val latestVersion: String? = null, val updatedAt: String, - val pushedAt: String?, ) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt index c4ca18360..6fa13a7b7 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt @@ -331,7 +331,6 @@ private fun PreviewDeveloperRepoItem() { isFavorite = false, latestVersion = "v1.5.2", updatedAt = Clock.System.now().toString(), - pushedAt = null, ), onItemClick = {}, onToggleFavorite = {}, From 49e1624e8a09eab719fd3bff77ac653432b5fd68 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:21 +0500 Subject: [PATCH 033/172] feat(strings): add design-refresh resource keys --- .../composeResources/values/strings.xml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 10dc48754..146bfa02a 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -366,6 +366,34 @@ Failed to load repositories View Details + Discover + Hot releases + Trending now + Most popular + From your stars + HOT · %1$s ago + #%1$d + Get + Back + Image + See all › + View all + View more + + Update available: %1$s → %2$s + What's changed since %1$s + via %1$s + sha-256 verified + change + View all %1$d releases + View all releases + View profile + Developer + Permissions + Stats + About + What's new + updated just now updated %1$d hour(s) ago updated yesterday @@ -1123,6 +1151,24 @@ updates ignored Ignore updates Discard + 1 update available + %1$d updates available + Review or update everything at once. + See list + Hide list + Ready to install + Pick an app + Select any app on the left to see its details and actions. + Status + Quick actions + Settings + Open repository + Installed + Latest + Pick variant + Advanced settings + Pick a repository + Tap any card on the left to view its details here. APK Inspect From e1836b4660298edb91fe4c74f60a4269fc172eb4 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:25 +0500 Subject: [PATCH 034/172] feat(theme): swap to Geist family, drop italic --- .../composeResources/font/geist.ttf | Bin 0 -> 169056 bytes .../composeResources/font/geist_mono.ttf | Bin 0 -> 171968 bytes .../rainxch/core/presentation/theme/Type.kt | 94 +++++++++--------- 3 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 core/presentation/src/commonMain/composeResources/font/geist.ttf create mode 100644 core/presentation/src/commonMain/composeResources/font/geist_mono.ttf diff --git a/core/presentation/src/commonMain/composeResources/font/geist.ttf b/core/presentation/src/commonMain/composeResources/font/geist.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f63f0afc6390323715972b6645115485f37cc9f4 GIT binary patch literal 169056 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

HN?lC-Oc-rs>!)u23 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

se!*q_486Mc}s4Sad7c0 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 >QBOL^LQe;P%i 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 literal 0 HcmV?d00001 diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt index 2e0590972..9ba7f13a6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Type.kt @@ -12,35 +12,37 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.Font import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.fraunces -import zed.rainxch.githubstore.core.presentation.res.fraunces_italic -import zed.rainxch.githubstore.core.presentation.res.inter_tight -import zed.rainxch.githubstore.core.presentation.res.jetbrains_mono +import zed.rainxch.githubstore.core.presentation.res.geist +import zed.rainxch.githubstore.core.presentation.res.geist_mono -val fraunces +val geist @Composable get() = FontFamily( - Font(Res.font.fraunces, FontWeight.Medium, FontStyle.Normal), - Font(Res.font.fraunces, FontWeight.SemiBold, FontStyle.Normal), - Font(Res.font.fraunces, FontWeight.Bold, FontStyle.Normal), - Font(Res.font.fraunces_italic, FontWeight.Medium, FontStyle.Italic), - Font(Res.font.fraunces_italic, FontWeight.SemiBold, FontStyle.Italic), - Font(Res.font.fraunces_italic, FontWeight.Bold, FontStyle.Italic), + Font(Res.font.geist, FontWeight.Normal, FontStyle.Normal), + Font(Res.font.geist, FontWeight.Medium, FontStyle.Normal), + Font(Res.font.geist, FontWeight.SemiBold, FontStyle.Normal), + Font(Res.font.geist, FontWeight.Bold, FontStyle.Normal), + Font(Res.font.geist, FontWeight.Black, FontStyle.Normal), ) -val interTight +val geistMono @Composable get() = FontFamily( - Font(Res.font.inter_tight, FontWeight.Normal), - Font(Res.font.inter_tight, FontWeight.Medium), - Font(Res.font.inter_tight, FontWeight.SemiBold), - Font(Res.font.inter_tight, FontWeight.Bold), + Font(Res.font.geist_mono, FontWeight.Normal), + Font(Res.font.geist_mono, FontWeight.Medium), + Font(Res.font.geist_mono, FontWeight.SemiBold), + Font(Res.font.geist_mono, FontWeight.Bold), ) +@Deprecated("Use geist", ReplaceWith("geist")) +val fraunces + @Composable get() = geist + +@Deprecated("Use geist", ReplaceWith("geist")) +val interTight + @Composable get() = geist + +@Deprecated("Use geistMono", ReplaceWith("geistMono")) val jetbrainsMono - @Composable get() = FontFamily( - Font(Res.font.jetbrains_mono, FontWeight.Normal), - Font(Res.font.jetbrains_mono, FontWeight.Medium), - Font(Res.font.jetbrains_mono, FontWeight.Bold), - ) + @Composable get() = geistMono private val baseline = Typography() @@ -48,42 +50,42 @@ private val baseline = Typography() fun getAppTypography(fontTheme: FontTheme = FontTheme.CUSTOM): Typography { if (fontTheme == FontTheme.SYSTEM) return baseline - val serif = fraunces - val sans = interTight + val family = geist - fun TextStyle.fraunces(weight: FontWeight) = copy( - fontFamily = serif, + fun TextStyle.display(weight: FontWeight) = copy( + fontFamily = family, fontWeight = weight, - fontStyle = FontStyle.Italic, - letterSpacing = (-0.02).em, + fontStyle = FontStyle.Normal, + letterSpacing = (-0.022).em, + textDecoration = TextDecoration.None, ) - fun TextStyle.sans(weight: FontWeight) = copy( - fontFamily = sans, + fun TextStyle.body(weight: FontWeight) = copy( + fontFamily = family, fontWeight = weight, + fontStyle = FontStyle.Normal, textDecoration = TextDecoration.None, ) return Typography( - - displayLarge = baseline.displayLarge.fraunces(FontWeight.SemiBold).copy( - letterSpacing = (-0.025).em, + displayLarge = baseline.displayLarge.display(FontWeight.Bold).copy( + letterSpacing = (-0.028).em, fontSize = 36.sp, ), - displayMedium = baseline.displayMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 32.sp), - displaySmall = baseline.displaySmall.fraunces(FontWeight.SemiBold).copy(fontSize = 28.sp), - headlineLarge = baseline.headlineLarge.fraunces(FontWeight.SemiBold).copy(fontSize = 26.sp), - headlineMedium = baseline.headlineMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 22.sp), - headlineSmall = baseline.headlineSmall.fraunces(FontWeight.SemiBold).copy(fontSize = 20.sp), - titleLarge = baseline.titleLarge.fraunces(FontWeight.SemiBold).copy(fontSize = 18.sp), - titleMedium = baseline.titleMedium.fraunces(FontWeight.SemiBold).copy(fontSize = 16.sp), - titleSmall = baseline.titleSmall.fraunces(FontWeight.SemiBold).copy(fontSize = 14.sp), + displayMedium = baseline.displayMedium.display(FontWeight.Bold).copy(fontSize = 32.sp), + displaySmall = baseline.displaySmall.display(FontWeight.Bold).copy(fontSize = 28.sp), + headlineLarge = baseline.headlineLarge.display(FontWeight.SemiBold).copy(fontSize = 26.sp), + headlineMedium = baseline.headlineMedium.display(FontWeight.SemiBold).copy(fontSize = 22.sp), + headlineSmall = baseline.headlineSmall.display(FontWeight.SemiBold).copy(fontSize = 20.sp), + titleLarge = baseline.titleLarge.display(FontWeight.SemiBold).copy(fontSize = 18.sp), + titleMedium = baseline.titleMedium.display(FontWeight.SemiBold).copy(fontSize = 16.sp), + titleSmall = baseline.titleSmall.display(FontWeight.SemiBold).copy(fontSize = 14.sp), - bodyLarge = baseline.bodyLarge.sans(FontWeight.Normal).copy(fontSize = 14.sp), - bodyMedium = baseline.bodyMedium.sans(FontWeight.Normal).copy(fontSize = 13.sp), - bodySmall = baseline.bodySmall.sans(FontWeight.Medium).copy(fontSize = 12.sp), - labelLarge = baseline.labelLarge.sans(FontWeight.SemiBold), - labelMedium = baseline.labelMedium.sans(FontWeight.SemiBold), - labelSmall = baseline.labelSmall.sans(FontWeight.SemiBold), + bodyLarge = baseline.bodyLarge.body(FontWeight.Normal).copy(fontSize = 14.sp), + bodyMedium = baseline.bodyMedium.body(FontWeight.Normal).copy(fontSize = 13.sp), + bodySmall = baseline.bodySmall.body(FontWeight.Medium).copy(fontSize = 12.sp), + labelLarge = baseline.labelLarge.body(FontWeight.SemiBold), + labelMedium = baseline.labelMedium.body(FontWeight.SemiBold), + labelSmall = baseline.labelSmall.body(FontWeight.SemiBold), ) } From 0e05bd5ae1601f91a19e2647c3bd6a09f09205e9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:34 +0500 Subject: [PATCH 035/172] feat(color): off-thread avatar dominant color cache --- .../color/DominantColorFromImage.android.kt | 33 +++++++++++++ .../presentation/color/AvatarColorStore.kt | 34 +++++++++++++ .../presentation/color/DominantColorMath.kt | 49 +++++++++++++++++++ .../presentation/color/RememberAvatarColor.kt | 10 ++++ .../components/GitHubStoreImage.kt | 18 +++++-- .../color/DominantColorFromImage.jvm.kt | 38 ++++++++++++++ 6 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.android.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/AvatarColorStore.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/DominantColorMath.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/RememberAvatarColor.kt create mode 100644 core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.jvm.kt diff --git a/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.android.kt b/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.android.kt new file mode 100644 index 000000000..7af6f1634 --- /dev/null +++ b/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.android.kt @@ -0,0 +1,33 @@ +package zed.rainxch.core.presentation.color + +import android.graphics.Bitmap +import androidx.compose.ui.graphics.Color +import androidx.core.graphics.scale +import coil3.BitmapImage +import coil3.Image + +private const val SampleSize = 48 + +actual fun computeDominantFromImage(image: Image): Color? { + val src = (image as? BitmapImage)?.bitmap ?: return null + val software = if (src.config == Bitmap.Config.HARDWARE) { + src.copy(Bitmap.Config.ARGB_8888, false) ?: return null + } else if (src.config != Bitmap.Config.ARGB_8888) { + src.copy(Bitmap.Config.ARGB_8888, false) ?: src + } else { + src + } + val sampled: Bitmap = if (software.width > SampleSize || software.height > SampleSize) { + software.scale(SampleSize, SampleSize, filter = true) + } else { + software + } + val w = sampled.width + val h = sampled.height + val pixels = IntArray(w * h) + val pxResult = runCatching { + sampled.getPixels(pixels, 0, w, 0, 0, w, h) + } + if (pxResult.isFailure) return null + return dominantFromArgb(pixels) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/AvatarColorStore.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/AvatarColorStore.kt new file mode 100644 index 000000000..37af0c8c0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/AvatarColorStore.kt @@ -0,0 +1,34 @@ +package zed.rainxch.core.presentation.color + +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.ui.graphics.Color +import coil3.Image +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +object AvatarColorStore { + private val cache: SnapshotStateMap = mutableStateMapOf() + private val inflight: MutableSet = mutableSetOf() + private val mutex = Mutex() + + fun colorFor(url: String): Color? = cache[url] + + suspend fun computeIfAbsent(url: String, image: Image) { + mutex.withLock { + if (cache.containsKey(url) || url in inflight) return + inflight.add(url) + } + val color = withContext(Dispatchers.Default) { + runCatching { computeDominantFromImage(image) }.getOrNull() + } + mutex.withLock { + inflight.remove(url) + if (color != null) cache[url] = color + } + } +} + +expect fun computeDominantFromImage(image: Image): Color? diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/DominantColorMath.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/DominantColorMath.kt new file mode 100644 index 000000000..d1d0ed7d4 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/DominantColorMath.kt @@ -0,0 +1,49 @@ +package zed.rainxch.core.presentation.color + +import androidx.compose.ui.graphics.Color + +internal fun dominantFromArgb(pixels: IntArray): Color? { + val strict = averageColor(pixels, lumMin = 24, lumMax = 232, chromaMin = 20) + if (strict != null) return strict + val relaxed = averageColor(pixels, lumMin = 10, lumMax = 245, chromaMin = 8) + if (relaxed != null) return relaxed + val anyColor = averageColor(pixels, lumMin = 4, lumMax = 251, chromaMin = 0) + return anyColor +} + +private fun averageColor( + pixels: IntArray, + lumMin: Int, + lumMax: Int, + chromaMin: Int, +): Color? { + var rSum = 0L + var gSum = 0L + var bSum = 0L + var count = 0L + for (px in pixels) { + val alpha = (px ushr 24) and 0xFF + if (alpha < 128) continue + val r = (px ushr 16) and 0xFF + val g = (px ushr 8) and 0xFF + val b = px and 0xFF + val luminance = (r + g + b) / 3 + if (luminance < lumMin || luminance > lumMax) continue + if (chromaMin > 0) { + val maxC = maxOf(r, g, b) + val minC = minOf(r, g, b) + if (maxC - minC < chromaMin) continue + } + rSum += r + gSum += g + bSum += b + count += 1 + } + if (count < 6L) return null + return Color( + red = (rSum / count).toInt(), + green = (gSum / count).toInt(), + blue = (bSum / count).toInt(), + alpha = 255, + ) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/RememberAvatarColor.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/RememberAvatarColor.kt new file mode 100644 index 000000000..94605b463 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/color/RememberAvatarColor.kt @@ -0,0 +1,10 @@ +package zed.rainxch.core.presentation.color + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun avatarColorFor(url: String?, fallback: Color): Color { + if (url.isNullOrBlank()) return fallback + return AvatarColorStore.colorFor(url) ?: fallback +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GitHubStoreImage.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GitHubStoreImage.kt index 02a41408c..3989056eb 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GitHubStoreImage.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/GitHubStoreImage.kt @@ -2,8 +2,6 @@ package zed.rainxch.core.presentation.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.CircularWavyProgressIndicator @@ -11,20 +9,24 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp import com.skydoves.landscapist.coil3.CoilImage +import com.skydoves.landscapist.coil3.CoilImageState import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.crossfade.CrossfadePlugin +import kotlinx.coroutines.launch +import zed.rainxch.core.presentation.color.AvatarColorStore @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun GitHubStoreImage( imageModel: () -> Any?, modifier: Modifier = Modifier, + extractDominantFor: String? = null, ) { + val scope = rememberCoroutineScope() CoilImage( imageModel = imageModel, modifier = modifier, @@ -49,6 +51,14 @@ fun GitHubStoreImage( ) } }, + onImageStateChanged = { state -> + if (extractDominantFor != null && state is CoilImageState.Success) { + val image = state.image + if (image != null) { + scope.launch { AvatarColorStore.computeIfAbsent(extractDominantFor, image) } + } + } + }, component = rememberImageComponent { CrossfadePlugin() diff --git a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.jvm.kt b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.jvm.kt new file mode 100644 index 000000000..158cf6b33 --- /dev/null +++ b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/color/DominantColorFromImage.jvm.kt @@ -0,0 +1,38 @@ +package zed.rainxch.core.presentation.color + +import androidx.compose.ui.graphics.Color +import coil3.Image +import coil3.toBitmap +import org.jetbrains.skia.ColorAlphaType +import org.jetbrains.skia.ColorType +import org.jetbrains.skia.ImageInfo + +private const val SampleSize = 48 + +actual fun computeDominantFromImage(image: Image): Color? { + val w = SampleSize + val h = SampleSize + val bitmap = runCatching { image.toBitmap(w, h) }.getOrNull() ?: return null + val info = ImageInfo( + width = w, + height = h, + colorType = ColorType.BGRA_8888, + alphaType = ColorAlphaType.UNPREMUL, + ) + val bytes = runCatching { + bitmap.readPixels(dstInfo = info, dstRowBytes = w * 4, srcX = 0, srcY = 0) + }.getOrNull() ?: return null + val pixels = IntArray(w * h) + var i = 0 + var p = 0 + while (i < pixels.size && p + 3 < bytes.size) { + val b = bytes[p].toInt() and 0xFF + val g = bytes[p + 1].toInt() and 0xFF + val r = bytes[p + 2].toInt() and 0xFF + val a = bytes[p + 3].toInt() and 0xFF + pixels[i] = (a shl 24) or (r shl 16) or (g shl 8) or b + i += 1 + p += 4 + } + return dominantFromArgb(pixels) +} From 68dd7b55127e6d5e500e1e34d1802ae6d95b5210 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:37 +0500 Subject: [PATCH 036/172] feat(vocabulary): real Apple and Tux platform glyphs --- .../presentation/vocabulary/PlatformGlyph.kt | 118 ++++++++++++++---- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt index d1569218f..81bdfaede 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/PlatformGlyph.kt @@ -35,7 +35,7 @@ fun PlatformGlyph( PlatformKind.ANDROID -> drawAndroid(color, bg, supported, alpha) PlatformKind.WINDOWS -> drawWindows(color, supported, alpha) PlatformKind.MACOS -> drawMacos(color, supported, alpha) - PlatformKind.LINUX -> drawLinux(color, supported, alpha) + PlatformKind.LINUX -> drawLinux(color, bg, supported, alpha) } } } @@ -97,41 +97,109 @@ private fun DrawScope.drawWindows(c: Color, on: Boolean, alpha: Float) { private fun DrawScope.drawMacos(c: Color, on: Boolean, alpha: Float) { val s = size.minDimension + val u = s / 24f val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) - val center = Offset(s * (12f / 24f), s * (13f / 24f)) - val radius = s * (7f / 24f) + + val body = Path().apply { + moveTo(7f * u, 7.5f * u) + cubicTo(4.5f * u, 7.5f * u, 2.5f * u, 10f * u, 2.5f * u, 13.5f * u) + cubicTo(2.5f * u, 17.5f * u, 4.5f * u, 21.5f * u, 7f * u, 21.5f * u) + cubicTo(8.2f * u, 21.5f * u, 9f * u, 20.8f * u, 10.5f * u, 20.8f * u) + cubicTo(12f * u, 20.8f * u, 12.5f * u, 21.5f * u, 14f * u, 21.5f * u) + cubicTo(15.3f * u, 21.5f * u, 16f * u, 21f * u, 17f * u, 20f * u) + cubicTo(18.5f * u, 18.4f * u, 19f * u, 16.5f * u, 19f * u, 16.4f * u) + cubicTo(18.9f * u, 16.4f * u, 16.5f * u, 15.5f * u, 16.5f * u, 12.8f * u) + cubicTo(16.5f * u, 10.6f * u, 18.3f * u, 9.4f * u, 18.4f * u, 9.3f * u) + cubicTo(17.3f * u, 7.7f * u, 15.6f * u, 7.5f * u, 15f * u, 7.5f * u) + cubicTo(13.5f * u, 7.4f * u, 12.2f * u, 8.3f * u, 11.5f * u, 8.3f * u) + cubicTo(10.8f * u, 8.3f * u, 9.7f * u, 7.5f * u, 8.5f * u, 7.5f * u) + cubicTo(8f * u, 7.5f * u, 7.5f * u, 7.5f * u, 7f * u, 7.5f * u) + close() + } + val leaf = Path().apply { + moveTo(11.4f * u, 6f * u) + cubicTo(12f * u, 4.6f * u, 13.1f * u, 3.8f * u, 14f * u, 3.6f * u) + cubicTo(14.3f * u, 4.7f * u, 14f * u, 6f * u, 13.3f * u, 6.9f * u) + cubicTo(12.7f * u, 7.7f * u, 11.7f * u, 8.2f * u, 10.8f * u, 8.2f * u) + cubicTo(10.6f * u, 7.4f * u, 10.9f * u, 6.7f * u, 11.4f * u, 6f * u) + close() + } if (on) { - drawCircle(color = c.copy(alpha = alpha), radius = radius, center = center) + drawPath(body, c.copy(alpha = alpha)) + drawPath(leaf, c.copy(alpha = alpha)) } else { - drawCircle( - color = c.copy(alpha = alpha), - radius = radius, - center = center, - style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash), - ) + val stroke = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash, join = StrokeJoin.Round) + drawPath(body, c.copy(alpha = alpha), style = stroke) + drawPath(leaf, c.copy(alpha = alpha), style = stroke) } - val stem = Path().apply { - moveTo(s * (12f / 24f), s * (6f / 24f)) - quadraticTo(s * (13.5f / 24f), s * (4f / 24f), s * (15f / 24f), s * (4f / 24f)) - } - drawPath(stem, c.copy(alpha = alpha), style = Stroke(width = (if (on) 1.5f else 1.4f).dp.toPx(), pathEffect = dash)) } -private fun DrawScope.drawLinux(c: Color, on: Boolean, alpha: Float) { +private fun DrawScope.drawLinux(c: Color, bg: Color, on: Boolean, alpha: Float) { val s = size.minDimension + val u = s / 24f val dash = if (on) null else PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx())) - val hex = Path().apply { - moveTo(s * (12f / 24f), s * (3.5f / 24f)) - lineTo(s * (19.5f / 24f), s * (7.5f / 24f)) - lineTo(s * (19.5f / 24f), s * (16.5f / 24f)) - lineTo(s * (12f / 24f), s * (20.5f / 24f)) - lineTo(s * (4.5f / 24f), s * (16.5f / 24f)) - lineTo(s * (4.5f / 24f), s * (7.5f / 24f)) + + val body = Path().apply { + moveTo(12f * u, 2.5f * u) + cubicTo(9f * u, 2.5f * u, 7.5f * u, 4.5f * u, 7.5f * u, 7.5f * u) + cubicTo(7.5f * u, 8.5f * u, 7.8f * u, 9.2f * u, 8.1f * u, 9.8f * u) + cubicTo(6.5f * u, 10.3f * u, 5.2f * u, 12.2f * u, 5.2f * u, 14.5f * u) + cubicTo(5.2f * u, 17.5f * u, 6.2f * u, 20f * u, 8f * u, 21f * u) + cubicTo(9f * u, 21.7f * u, 10.5f * u, 22f * u, 12f * u, 22f * u) + cubicTo(13.5f * u, 22f * u, 15f * u, 21.7f * u, 16f * u, 21f * u) + cubicTo(17.8f * u, 20f * u, 18.8f * u, 17.5f * u, 18.8f * u, 14.5f * u) + cubicTo(18.8f * u, 12.2f * u, 17.5f * u, 10.3f * u, 15.9f * u, 9.8f * u) + cubicTo(16.2f * u, 9.2f * u, 16.5f * u, 8.5f * u, 16.5f * u, 7.5f * u) + cubicTo(16.5f * u, 4.5f * u, 15f * u, 2.5f * u, 12f * u, 2.5f * u) close() } + + val belly = Path().apply { + moveTo(12f * u, 12.5f * u) + cubicTo(10.3f * u, 12.5f * u, 9.5f * u, 14.5f * u, 9.5f * u, 16.8f * u) + cubicTo(9.5f * u, 19f * u, 10.5f * u, 20.5f * u, 12f * u, 20.5f * u) + cubicTo(13.5f * u, 20.5f * u, 14.5f * u, 19f * u, 14.5f * u, 16.8f * u) + cubicTo(14.5f * u, 14.5f * u, 13.7f * u, 12.5f * u, 12f * u, 12.5f * u) + close() + } + + val beak = Path().apply { + moveTo(10.4f * u, 7.8f * u) + lineTo(13.6f * u, 7.8f * u) + lineTo(12f * u, 9.4f * u) + close() + } + + val leftFoot = Path().apply { + moveTo(7f * u, 21.5f * u) + cubicTo(5.5f * u, 21.5f * u, 4.5f * u, 22f * u, 4.5f * u, 22.6f * u) + cubicTo(4.5f * u, 23.2f * u, 6.5f * u, 23.5f * u, 8.5f * u, 23f * u) + cubicTo(9.5f * u, 22.7f * u, 9.5f * u, 21.8f * u, 9f * u, 21.4f * u) + close() + } + val rightFoot = Path().apply { + moveTo(17f * u, 21.5f * u) + cubicTo(18.5f * u, 21.5f * u, 19.5f * u, 22f * u, 19.5f * u, 22.6f * u) + cubicTo(19.5f * u, 23.2f * u, 17.5f * u, 23.5f * u, 15.5f * u, 23f * u) + cubicTo(14.5f * u, 22.7f * u, 14.5f * u, 21.8f * u, 15f * u, 21.4f * u) + close() + } + if (on) { - drawPath(hex, c.copy(alpha = alpha)) + drawPath(leftFoot, c.copy(alpha = alpha)) + drawPath(rightFoot, c.copy(alpha = alpha)) + drawPath(body, c.copy(alpha = alpha)) + drawPath(belly, bg) + drawPath(beak, c.copy(alpha = alpha)) + drawCircle(bg, s * (0.85f / 24f), Offset(10.4f * u, 6.4f * u)) + drawCircle(bg, s * (0.85f / 24f), Offset(13.6f * u, 6.4f * u)) + drawCircle(c.copy(alpha = alpha), s * (0.35f / 24f), Offset(10.4f * u, 6.4f * u)) + drawCircle(c.copy(alpha = alpha), s * (0.35f / 24f), Offset(13.6f * u, 6.4f * u)) } else { - drawPath(hex, c.copy(alpha = alpha), style = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash, join = StrokeJoin.Round)) + val stroke = Stroke(width = 1.4f.dp.toPx(), pathEffect = dash, join = StrokeJoin.Round) + drawPath(leftFoot, c.copy(alpha = alpha), style = stroke) + drawPath(rightFoot, c.copy(alpha = alpha), style = stroke) + drawPath(body, c.copy(alpha = alpha), style = stroke) + drawPath(belly, c.copy(alpha = alpha), style = Stroke(width = 1f.dp.toPx(), pathEffect = dash)) } } From 67b87928f17e3bdcb24dc1aacf43d9afeda9fe43 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:45 +0500 Subject: [PATCH 037/172] feat(core): RepoStripeCard, FloatingPill, chips, Ember palette --- .../presentation/components/FloatingPill.kt | 37 +++ .../components/cards/RepoStripeCard.kt | 220 ++++++++++++++++++ .../components/chips/PlatformsChip.kt | 42 ++++ .../presentation/components/chips/StatChip.kt | 48 ++++ .../presentation/theme/tokens/EmberPalette.kt | 11 + 5 files changed, 358 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RepoStripeCard.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/PlatformsChip.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/StatChip.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/EmberPalette.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt new file mode 100644 index 000000000..76a659927 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt @@ -0,0 +1,37 @@ +package zed.rainxch.core.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun FloatingPill( + modifier: Modifier = Modifier, + horizontalPadding: Dp = 12.dp, + verticalPadding: Dp = 10.dp, + content: @Composable () -> Unit, +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(50), + ) + .padding(horizontal = horizontalPadding, vertical = verticalPadding), + contentAlignment = Alignment.Center, + ) { + content() + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RepoStripeCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RepoStripeCard.kt new file mode 100644 index 000000000..dfcb075f8 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/RepoStripeCard.kt @@ -0,0 +1,220 @@ +package zed.rainxch.core.presentation.components.cards + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.lerp +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.shapes.CornerRadii +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape + +private val RepoStripeShape = WonkySquircleShape( + topStart = CornerRadii(28.dp, 22.dp), + topEnd = CornerRadii(22.dp, 28.dp), + bottomEnd = CornerRadii(28.dp, 22.dp), + bottomStart = CornerRadii(22.dp, 28.dp), +) + +private fun Color.normalizedForStripe(isDark: Boolean): Color { + val r = (red * 255f).toInt().coerceIn(0, 255) + val g = (green * 255f).toInt().coerceIn(0, 255) + val b = (blue * 255f).toInt().coerceIn(0, 255) + val lum = (r + g + b) / 3 + val target = if (isDark) 170 else 140 + val minLum = if (isDark) 90 else 70 + if (lum >= minLum) return this + val factor = target.toFloat() / lum.coerceAtLeast(1).toFloat() + return Color( + red = (r * factor).toInt().coerceIn(0, 255), + green = (g * factor).toInt().coerceIn(0, 255), + blue = (b * factor).toInt().coerceIn(0, 255), + alpha = 255, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RepoStripeCard( + accent: Color, + ownerLogin: String, + name: String, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + stripeTrailing: (@Composable () -> Unit)? = null, + avatar: @Composable () -> Unit, + chips: @Composable () -> Unit, + languagePill: (@Composable () -> Unit)? = null, + cta: @Composable () -> Unit, +) { + val borderColor = MaterialTheme.colorScheme.outline + val isDark = isSystemInDarkTheme() + val surface = MaterialTheme.colorScheme.surface + val normalizedTarget = remember(accent, isDark) { accent.normalizedForStripe(isDark) } + val tintTargetFraction = if (isDark) 0.10f else 0.06f + val animatedAccent by animateColorAsState( + targetValue = normalizedTarget, + animationSpec = tween(durationMillis = 1800, easing = LinearOutSlowInEasing), + label = "repo-stripe-accent", + ) + val animatedSurface by animateColorAsState( + targetValue = lerp(surface, normalizedTarget, tintTargetFraction), + animationSpec = tween(durationMillis = 1800, easing = LinearOutSlowInEasing), + label = "repo-stripe-surface", + ) + val stripeBase = if (isDark) animatedAccent.copy(alpha = 0.18f) else animatedAccent.copy(alpha = 0.12f) + val stripeLineThick = if (isDark) animatedAccent.copy(alpha = 0.45f) else animatedAccent.copy(alpha = 0.55f) + val stripeLineThin = if (isDark) animatedAccent.copy(alpha = 0.22f) else animatedAccent.copy(alpha = 0.30f) + val avatarBg = if (isDark) animatedAccent.copy(alpha = 0.18f) else animatedAccent.copy(alpha = 0.14f) + + Box( + modifier = modifier + .fillMaxWidth() + .clip(RepoStripeShape) + .background(animatedSurface) + .border(width = 1.5.dp, color = borderColor, shape = RepoStripeShape) + .combinedClickable(onClick = onClick, onLongClick = onLongClick), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .clipToBounds() + .drawBehind { + drawRect(color = stripeBase) + val thick = 9.dp.toPx() + val thin = 2.5.dp.toPx() + val gapAfterThick = 10.dp.toPx() + val gapBetweenThin = 6.dp.toPx() + val cycle = thick + gapAfterThick + thin + gapBetweenThin + thin + gapAfterThick + var x = -size.height + while (x < size.width + size.height) { + val baseY = size.height + val endX = x + size.height + drawLine( + color = stripeLineThick, + start = Offset(x, baseY), + end = Offset(endX, 0f), + strokeWidth = thick, + cap = StrokeCap.Round, + ) + var xt = x + thick + gapAfterThick + drawLine( + color = stripeLineThin, + start = Offset(xt, baseY), + end = Offset(xt + size.height, 0f), + strokeWidth = thin, + cap = StrokeCap.Round, + ) + xt += thin + gapBetweenThin + drawLine( + color = stripeLineThin, + start = Offset(xt, baseY), + end = Offset(xt + size.height, 0f), + strokeWidth = thin, + cap = StrokeCap.Round, + ) + x += cycle + } + }, + ) { + if (stripeTrailing != null) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 14.dp), + ) { + stripeTrailing() + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 96.dp, end = 14.dp, top = 12.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = ownerLogin, + style = MaterialTheme.typography.labelSmall.copy( + fontSize = 12.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = name, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(10.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 1.dp, + ) + Spacer(Modifier.height(10.dp)) + chips() + if (languagePill != null) { + Spacer(Modifier.height(6.dp)) + languagePill() + } + } + cta() + } + } + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = 14.dp, y = 26.dp) + .size(72.dp) + .clip(CircleShape) + .background(avatarBg), + contentAlignment = Alignment.Center, + ) { + avatar() + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/PlatformsChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/PlatformsChip.kt new file mode 100644 index 000000000..657fc5ce6 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/PlatformsChip.kt @@ -0,0 +1,42 @@ +package zed.rainxch.core.presentation.components.chips + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.PlatformGlyph +import zed.rainxch.core.presentation.vocabulary.PlatformKind + +@Composable +fun PlatformsChip( + platforms: ImmutableList, + modifier: Modifier = Modifier, + background: Color = Color.Transparent, + border: Color = MaterialTheme.colorScheme.outline, + glyphSizeDp: Int = 14, +) { + if (platforms.isEmpty()) return + Row( + modifier = modifier + .clip(Radii.chip) + .background(background) + .border(width = 1.dp, color = border, shape = Radii.chip) + .padding(horizontal = 10.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(7.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = glyphSizeDp) + } + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/StatChip.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/StatChip.kt new file mode 100644 index 000000000..6c5361a59 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chips/StatChip.kt @@ -0,0 +1,48 @@ +package zed.rainxch.core.presentation.components.chips + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.tokens.Radii + +@Composable +fun StatChip( + label: String, + modifier: Modifier = Modifier, + leading: (@Composable () -> Unit)? = null, + background: Color = Color.Transparent, + border: Color = MaterialTheme.colorScheme.outline, + contentColor: Color = MaterialTheme.colorScheme.onSurface, +) { + Row( + modifier = modifier + .clip(Radii.chip) + .background(background) + .border(width = 1.dp, color = border, shape = Radii.chip) + .padding(horizontal = 10.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (leading != null) leading() + Text( + text = label, + color = contentColor, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), + ) + } +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/EmberPalette.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/EmberPalette.kt new file mode 100644 index 000000000..34701471e --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/EmberPalette.kt @@ -0,0 +1,11 @@ +package zed.rainxch.core.presentation.theme.tokens + +import androidx.compose.ui.graphics.Color + +object EmberPalette { + val Deep = Color(0xFFB3261E) + val Hot = Color(0xFFE65100) + val Warm = Color(0xFFFF9100) + val Amber = Color(0xFFFFC400) + val Ash = Color(0xFF2A1410) +} From e51176317c27ef8bdbbbc6aaa29bde78dbc4ccf9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:49 +0500 Subject: [PATCH 038/172] feat(adaptive): list-detail scaffold with resizable divider --- .../components/adaptive/AdaptiveListDetail.kt | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/adaptive/AdaptiveListDetail.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/adaptive/AdaptiveListDetail.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/adaptive/AdaptiveListDetail.kt new file mode 100644 index 000000000..b28246ef0 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/adaptive/AdaptiveListDetail.kt @@ -0,0 +1,243 @@ +package zed.rainxch.core.presentation.components.adaptive + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.OpenInBrowser +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Immutable +data class AdaptiveDetailArgs( + val repositoryId: Long = -1L, + val owner: String? = null, + val repo: String? = null, + val isComingFromUpdate: Boolean = false, + val sourceHost: String? = null, +) + +@Stable +class AdaptiveListDetailState( + initial: AdaptiveDetailArgs? = null, +) { + var currentArgs: AdaptiveDetailArgs? by mutableStateOf(initial) + private set + + fun select(args: AdaptiveDetailArgs) { + currentArgs = args + } + + fun clear() { + currentArgs = null + } +} + +@Composable +fun rememberAdaptiveListDetailState(): AdaptiveListDetailState = + rememberSaveable(saver = AdaptiveListDetailStateSaver) { + AdaptiveListDetailState() + } + +private val AdaptiveListDetailStateSaver: Saver = + Saver( + save = { state -> + val args = state.currentArgs ?: return@Saver booleanArrayOf(false) + listOf( + true, + args.repositoryId, + args.owner ?: "", + args.repo ?: "", + args.isComingFromUpdate, + args.sourceHost ?: "", + ) + }, + restore = { saved -> + if (saved is BooleanArray || saved !is List<*> || saved.isEmpty() || saved[0] != true) { + AdaptiveListDetailState(initial = null) + } else { + AdaptiveListDetailState( + initial = AdaptiveDetailArgs( + repositoryId = (saved[1] as? Long) ?: 0L, + owner = (saved[2] as? String)?.takeIf { it.isNotEmpty() }, + repo = (saved[3] as? String)?.takeIf { it.isNotEmpty() }, + isComingFromUpdate = (saved[4] as? Boolean) ?: false, + sourceHost = (saved[5] as? String)?.takeIf { it.isNotEmpty() }, + ), + ) + } + }, + ) + +@Composable +fun AdaptiveListDetailScaffold( + state: AdaptiveListDetailState, + list: @Composable (isExpanded: Boolean) -> Unit, + detail: @Composable (AdaptiveDetailArgs) -> Unit, + emptyPaneTitle: String, + emptyPaneSubtitle: String, + modifier: Modifier = Modifier, + initialListFraction: Float = 0.46f, + minListWidthDp: Int = 480, + minDetailWidthDp: Int = 600, + expandedBreakpointDp: Int = 840, +) { + BoxWithConstraints(modifier = modifier.fillMaxSize()) { + val totalWidthDp = maxWidth.value + val effectiveBreakpoint = + maxOf(expandedBreakpointDp.toFloat(), (minListWidthDp + minDetailWidthDp).toFloat()) + val isExpanded = totalWidthDp >= effectiveBreakpoint + if (!isExpanded) { + Box(modifier = Modifier.fillMaxSize()) { list(false) } + return@BoxWithConstraints + } + + val minList = minListWidthDp.toFloat() + val maxList = (totalWidthDp - minDetailWidthDp).coerceAtLeast(minList) + + var listWidthDp by rememberSaveable { + mutableFloatStateOf((totalWidthDp * initialListFraction).coerceIn(minList, maxList)) + } + val clamped = listWidthDp.coerceIn(minList, maxList) + val density = LocalDensity.current + + Row(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .width(clamped.dp) + .fillMaxHeight(), + ) { + list(true) + } + PaneResizeHandle( + onDrag = { deltaPx -> + val deltaDp = with(density) { deltaPx.toDp().value } + listWidthDp = (listWidthDp + deltaDp).coerceIn(minList, maxList) + }, + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) { + val args = state.currentArgs + if (args == null) { + EmptyDetailPlaceholder( + title = emptyPaneTitle, + subtitle = emptyPaneSubtitle, + ) + } else { + detail(args) + } + } + } + } +} + +@Composable +private fun PaneResizeHandle( + onDrag: (Float) -> Unit, +) { + val draggableState = rememberDraggableState(onDelta = onDrag) + Box( + modifier = Modifier + .fillMaxHeight() + .width(10.dp) + .pointerHoverIcon(PointerIcon.Crosshair) + .draggable( + state = draggableState, + orientation = Orientation.Horizontal, + ), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(1.dp) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), + ) + Box( + modifier = Modifier + .width(4.dp) + .height(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.85f)), + ) + } +} + +@Composable +private fun EmptyDetailPlaceholder( + title: String, + subtitle: String, +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(32.dp), + ) { + Icon( + imageVector = Icons.Outlined.OpenInBrowser, + contentDescription = null, + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(56.dp), + ) + Spacer(Modifier.height(14.dp)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + } + } +} + +fun AdaptiveDetailArgs.matches(other: AdaptiveDetailArgs): Boolean = + repositoryId == other.repositoryId && + owner == other.owner && + repo == other.repo && + sourceHost == other.sourceHost From bee29ea6e3a8ee5a5abdffff80ca8ac60828b74f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:50:54 +0500 Subject: [PATCH 039/172] feat(overlays): Apple-style GhsDropdownMenu, dialog polish --- .../components/overlays/GhsConfirmDialog.kt | 1 - .../components/overlays/GhsDropdownMenu.kt | 138 ++++++++++++++---- 2 files changed, 111 insertions(+), 28 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt index 3b1e51f7e..fc32162ea 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt @@ -53,7 +53,6 @@ fun GhsConfirmDialog( Text( text = title, style = MaterialTheme.typography.titleLarge.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 18.sp, ), diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt index adcf11d1f..bc559167e 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt @@ -1,65 +1,149 @@ package zed.rainxch.core.presentation.components.overlays +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import zed.rainxch.core.presentation.theme.tokens.Radii +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.PopupProperties @Composable fun GhsDropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, + offset: DpOffset = DpOffset(0.dp, 8.dp), + scrollState: ScrollState = rememberScrollState(), + properties: PopupProperties = PopupProperties(focusable = true), content: @Composable () -> Unit, ) { DropdownMenu( expanded = expanded, onDismissRequest = onDismissRequest, - modifier = modifier - .clip(Radii.cardSm) - .background(MaterialTheme.colorScheme.surface) - .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = Radii.cardSm), - ) { - Column(content = { content() }) - } + modifier = modifier.widthIn(min = 240.dp), + offset = offset, + scrollState = scrollState, + properties = properties, + shape = RoundedCornerShape(14.dp), + containerColor = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 0.dp, + shadowElevation = 18.dp, + border = BorderStroke( + width = 0.5.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.35f), + ), + content = { content() }, + ) } @Composable -fun GhsDropdownItem( - label: String, +fun GhsDropdownMenuItem( + text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - leading: (@Composable () -> Unit)? = null, - trailing: (@Composable () -> Unit)? = null, + enabled: Boolean = true, + subtitle: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + contentColor: Color = MaterialTheme.colorScheme.onSurface, + minHeight: Dp = 40.dp, ) { + val resolvedColor = if (enabled) contentColor else contentColor.copy(alpha = 0.38f) + val mutedColor = if (enabled) { + contentColor.copy(alpha = 0.55f) + } else { + contentColor.copy(alpha = 0.30f) + } + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val pressTint = if (pressed) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.10f) + } else { + Color.Transparent + } Row( modifier = modifier - .padding(horizontal = 14.dp, vertical = 10.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + .fillMaxWidth() + .background(pressTint) + .clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + .heightIn(min = minHeight) + .padding(horizontal = 14.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - leading?.invoke() - Text( - text = label, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyMedium, - ) - if (trailing != null) { - androidx.compose.foundation.layout.Spacer(modifier = Modifier.weight(1f)) - trailing.invoke() + if (leadingIcon != null) { + CompositionLocalProvider(LocalContentColor provides resolvedColor) { + leadingIcon() + } + } + androidx.compose.foundation.layout.Column(modifier = Modifier.weight(1f, fill = false)) { + CompositionLocalProvider(LocalContentColor provides resolvedColor) { + ProvideTextStyle( + value = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + ), + ) { + Text(text = text, color = resolvedColor) + } + } + if (subtitle != null) { + Text( + text = subtitle, + color = mutedColor, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp), + ) + } + } + if (trailingIcon != null) { + Spacer(modifier = Modifier.weight(1f)) + CompositionLocalProvider(LocalContentColor provides resolvedColor) { + trailingIcon() + } } } } -private fun Modifier.weight(@Suppress("UNUSED_PARAMETER") f: Float): Modifier = this +@Composable +fun GhsDropdownMenuDivider() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(0.5.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)), + ) +} From 1649023b6db13a4cc8faf7942d9ebbdc9347c659 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:00 +0500 Subject: [PATCH 040/172] fix(core): replace Unicode chevrons with vector icons --- .../presentation/components/RepositoryCard.kt | 18 ++++++++++++++++-- .../components/cards/VitalSignsGrid.kt | 1 - .../components/cards/WaxSealTrustCard.kt | 1 - .../components/section/SectionHeader.kt | 6 ++++-- .../core/presentation/utils/CountFormatter.kt | 8 ++++++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 0730d2676..6e5a95ee3 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.outlined.CallSplit import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Verified @@ -523,10 +524,15 @@ fun PlatformChip( onClick: (() -> Unit)? = null, ) { Surface( - modifier = - if (onClick != null) modifier.clickable(onClick = onClick) else modifier, + modifier = if (onClick != null) modifier.clickable(onClick = onClick) else modifier, shape = RoundedCornerShape(16.dp), color = MaterialTheme.colorScheme.surfaceContainerHighest, + border = if (onClick != null) { + androidx.compose.foundation.BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.35f), + ) + } else null, ) { FlowRow( modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), @@ -549,6 +555,14 @@ fun PlatformChip( fontWeight = FontWeight.Medium, modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), ) + if (onClick != null) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(14.dp).padding(end = 4.dp), + ) + } } } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt index 4bb6c07fe..b5a433b72 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/VitalSignsGrid.kt @@ -74,7 +74,6 @@ private fun VitalTileBox(tile: VitalTile, modifier: Modifier = Modifier) { Text( text = tile.value, style = MaterialTheme.typography.bodyLarge.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ), diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt index cfd9f997e..9e18a2f23 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt @@ -48,7 +48,6 @@ fun WaxSealTrustCard( Text( text = stateLabel, style = MaterialTheme.typography.titleMedium.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, ), diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt index fbf8165f1..a6c402fcb 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/section/SectionHeader.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.common_see_all import zed.rainxch.core.presentation.vocabulary.Squiggle @Composable @@ -38,7 +41,6 @@ fun SectionHeader( Text( text = title, style = MaterialTheme.typography.titleLarge.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 20.sp, ), @@ -55,7 +57,7 @@ fun SectionHeader( if (onSeeAll != null) { Text( - text = "See all ›", + text = stringResource(Res.string.common_see_all), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium.copy( fontWeight = FontWeight.SemiBold, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/CountFormatter.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/CountFormatter.kt index d898f62fd..4dc386512 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/CountFormatter.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/CountFormatter.kt @@ -11,3 +11,11 @@ fun formatCount(count: Int): String = count >= 1000 -> stringResource(Res.string.count_thousands, count / 1000) else -> count.toString() } + +@Composable +fun formatCount(count: Long): String = + when { + count >= 1_000_000L -> stringResource(Res.string.count_millions, (count / 1_000_000L).toInt()) + count >= 1000L -> stringResource(Res.string.count_thousands, (count / 1000L).toInt()) + else -> count.toString() + } From f3f150f99048ceb94d2ece791eec2d8d3d3e87f7 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:04 +0500 Subject: [PATCH 041/172] fix(home/data): preserve availablePlatforms in fan-out --- .../impl/CachedRepositoriesDataSourceImpl.kt | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt index 066ba61ae..248081474 100644 --- a/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt +++ b/feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/data_source/impl/CachedRepositoriesDataSourceImpl.kt @@ -164,17 +164,7 @@ class CachedRepositoriesDataSourceImpl( platforms.map { plat -> async { val r = backendApiClient.getCategory(categorySlug, plat) - val discoveryPlatform = when (plat) { - "android" -> DiscoveryPlatform.Android - "windows" -> DiscoveryPlatform.Windows - "macos" -> DiscoveryPlatform.Macos - "linux" -> DiscoveryPlatform.Linux - else -> return@async null - } - r.getOrNull()?.map { - it.toCachedGithubRepoSummary() - .copy(availablePlatforms = listOf(discoveryPlatform)) - } + r.getOrNull()?.map { it.toCachedGithubRepoSummary() } } }.awaitAll().filterNotNull() } @@ -245,16 +235,8 @@ class CachedRepositoriesDataSourceImpl( val responses = coroutineScope { platforms.map { plat -> async { - val discoveryPlatform = when (plat) { - "android" -> DiscoveryPlatform.Android - "windows" -> DiscoveryPlatform.Windows - "macos" -> DiscoveryPlatform.Macos - "linux" -> DiscoveryPlatform.Linux - else -> return@async null - } backendApiClient.getTopic(topicSlug, plat).getOrNull()?.map { it.toCachedGithubRepoSummary() - .copy(availablePlatforms = listOf(discoveryPlatform)) } } }.awaitAll().filterNotNull() @@ -452,8 +434,12 @@ class CachedRepositoriesDataSourceImpl( json.decodeFromString(response.bodyAsText()) .let { repoResponse -> repoResponse.copy( - repositories = repoResponse.repositories.map { - it.copy(availablePlatforms = listOf(filePlatform)) + repositories = repoResponse.repositories.map { repo -> + if (repo.availablePlatforms.isEmpty()) { + repo.copy(availablePlatforms = listOf(filePlatform)) + } else { + repo + } }, ) } From 099e1a5cb9440a605044238945a82d7974e98ae3 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:11 +0500 Subject: [PATCH 042/172] feat(home): redesigned cards with avatar accent and platform glyphs --- .../zed/rainxch/home/presentation/HomeRoot.kt | 30 +- .../categorylist/CategoryListRoot.kt | 23 +- .../presentation/components/HomeTopBar.kt | 6 +- .../presentation/components/HotCardItem.kt | 183 ++++++++--- .../home/presentation/components/LeadCard.kt | 284 +++++++++++++----- .../presentation/components/SeeAllTile.kt | 129 ++++++++ .../presentation/components/StarredRowItem.kt | 29 +- .../components/TrendingRowItem.kt | 179 +++++++---- .../presentation/model/HomeRepoCardMapper.kt | 2 + .../home/presentation/model/HomeRepoCardUi.kt | 2 + 10 files changed, 654 insertions(+), 213 deletions(-) create mode 100644 feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/SeeAllTile.kt diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 7052826d3..4f114c518 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -41,12 +41,18 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.home_finding_repositories import zed.rainxch.githubstore.core.presentation.res.home_retry +import zed.rainxch.githubstore.core.presentation.res.home_section_from_your_stars +import zed.rainxch.githubstore.core.presentation.res.home_section_hot_releases +import zed.rainxch.githubstore.core.presentation.res.home_section_most_popular +import zed.rainxch.githubstore.core.presentation.res.home_section_trending_now import zed.rainxch.home.domain.model.HomeCategory import zed.rainxch.home.presentation.components.HomeTopBar import zed.rainxch.home.presentation.components.HotCardItem import zed.rainxch.home.presentation.components.LeadCard import zed.rainxch.home.presentation.components.PopularRowItem import zed.rainxch.home.presentation.components.RepositoryActionsSheet +import zed.rainxch.home.presentation.components.SeeAllHotTile +import zed.rainxch.home.presentation.components.SeeMoreRow import zed.rainxch.home.presentation.components.StarredRowItem import zed.rainxch.home.presentation.components.TrendingRowItem @@ -90,7 +96,6 @@ fun HomeRoot( else -> viewModel.onAction(action) } }, - viewModel = viewModel, ) } @@ -100,7 +105,6 @@ private fun HomeScreen( state: HomeState, snackbarHost: SnackbarHostState, onAction: (HomeAction) -> Unit, - viewModel: HomeViewModel, ) { val bottomNavHeight = LocalBottomNavigationHeight.current val listState = rememberLazyListState() @@ -188,7 +192,7 @@ private fun HomeScreen( if (state.hot.isNotEmpty()) { item(key = "hot_header") { SectionHeader( - title = "Hot releases", + title = stringResource(Res.string.home_section_hot_releases), subCount = state.hot.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllHot) }, ) @@ -205,6 +209,11 @@ private fun HomeScreen( onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, ) } + item(key = "hot_see_all") { + SeeAllHotTile( + onClick = { onAction(HomeAction.OnSeeAllHot) }, + ) + } } } } @@ -212,7 +221,7 @@ private fun HomeScreen( if (state.trending.isNotEmpty()) { item(key = "trending_header") { SectionHeader( - title = "Trending now", + title = stringResource(Res.string.home_section_trending_now), subCount = state.trending.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllTrending) }, ) @@ -228,12 +237,15 @@ private fun HomeScreen( onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, ) } + item(key = "trending_see_more") { + SeeMoreRow(onClick = { onAction(HomeAction.OnSeeAllTrending) }) + } } if (state.popular.isNotEmpty()) { item(key = "popular_header") { SectionHeader( - title = "Most popular", + title = stringResource(Res.string.home_section_most_popular), subCount = state.popular.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllPopular) }, ) @@ -249,12 +261,15 @@ private fun HomeScreen( onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, ) } + item(key = "popular_see_more") { + SeeMoreRow(onClick = { onAction(HomeAction.OnSeeAllPopular) }) + } } if (state.isUserSignedIn && state.starred.isNotEmpty()) { item(key = "starred_header") { SectionHeader( - title = "From your stars", + title = stringResource(Res.string.home_section_from_your_stars), subCount = state.starred.size.toString(), onSeeAll = { onAction(HomeAction.OnSeeAllStarred) }, ) @@ -266,6 +281,9 @@ private fun HomeScreen( onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, ) } + item(key = "starred_see_more") { + SeeMoreRow(onClick = { onAction(HomeAction.OnSeeAllStarred) }) + } } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt index 9f797120d..2941a74a6 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/categorylist/CategoryListRoot.kt @@ -32,8 +32,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.cd_back +import zed.rainxch.githubstore.core.presentation.res.home_section_hot_releases +import zed.rainxch.githubstore.core.presentation.res.home_section_most_popular +import zed.rainxch.githubstore.core.presentation.res.home_section_trending_now import zed.rainxch.core.presentation.components.buttons.IconButton import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.vocabulary.Squiggle @@ -146,15 +152,20 @@ private fun CategoryListTopBar(category: HomeCategory, onBack: () -> Unit) { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(Res.string.cd_back), tint = MaterialTheme.colorScheme.onSurface, ) } Column(modifier = Modifier.padding(start = 4.dp)) { Text( - text = categoryTitle(category), + text = stringResource( + when (category) { + HomeCategory.HOT_RELEASE -> Res.string.home_section_hot_releases + HomeCategory.TRENDING -> Res.string.home_section_trending_now + HomeCategory.MOST_POPULAR -> Res.string.home_section_most_popular + }, + ), style = MaterialTheme.typography.headlineSmall.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 26.sp, ), @@ -165,9 +176,3 @@ private fun CategoryListTopBar(category: HomeCategory, onBack: () -> Unit) { } } } - -private fun categoryTitle(category: HomeCategory): String = when (category) { - HomeCategory.HOT_RELEASE -> "Hot releases" - HomeCategory.TRENDING -> "Trending now" - HomeCategory.MOST_POPULAR -> "Most popular" -} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt index f5ee0b325..45c925762 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt @@ -13,6 +13,9 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_topbar_discover @Composable fun HomeTopBar( @@ -26,9 +29,8 @@ fun HomeTopBar( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Text( - text = "Discover", + text = stringResource(Res.string.home_topbar_discover), style = MaterialTheme.typography.displaySmall.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 28.sp, ), diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt index 84cf50dc5..44c6b7dec 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HotCardItem.kt @@ -1,13 +1,21 @@ package zed.rainxch.home.presentation.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -15,27 +23,42 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.LocalFireDepartment +import androidx.compose.material.icons.outlined.Star import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.cards.CompactCard -import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.theme.shapes.CornerRadii +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.EmberPalette +import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.core.presentation.vocabulary.FreshnessRing import zed.rainxch.core.presentation.vocabulary.PlatformGlyph -import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.core.presentation.vocabulary.TopicGlyph import zed.rainxch.home.presentation.model.HomeRepoCardUi +private val HotCardShape = WonkySquircleShape( + topStart = CornerRadii(24.dp, 18.dp), + topEnd = CornerRadii(18.dp, 24.dp), + bottomEnd = CornerRadii(24.dp, 18.dp), + bottomStart = CornerRadii(18.dp, 24.dp), +) + @OptIn(ExperimentalFoundationApi::class) @Composable fun HotCardItem( @@ -44,14 +67,28 @@ fun HotCardItem( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { + val transition = rememberInfiniteTransition(label = "hot-row-flame") + val flamePulse by transition.animateFloat( + initialValue = 0.65f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1100, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "hot-row-flame-pulse", + ) + Box( modifier = modifier - .width(260.dp) - .height(200.dp) - .clip(Radii.card) - .combinedClickable(onClick = onClick, onLongClick = onLongClick), + .width(280.dp) + .height(186.dp) + .clip(HotCardShape) + .background(MaterialTheme.colorScheme.surface) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = HotCardShape) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding(horizontal = 14.dp, vertical = 12.dp), ) { - CompactCard(modifier = Modifier.height(200.dp)) { + Column(modifier = Modifier.fillMaxSize()) { Row( verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -59,73 +96,135 @@ fun HotCardItem( ) { FreshnessRing( daysSinceRelease = card.daysSinceUpdate, - sizeDp = 44, - color = card.accentSaturated, + sizeDp = 42, + color = MaterialTheme.colorScheme.primary, ) { GitHubStoreImage( imageModel = { card.ownerAvatarUrl }, - modifier = Modifier.size(44.dp).clip(CircleShape), + modifier = Modifier.size(42.dp).clip(CircleShape), + extractDominantFor = card.ownerAvatarUrl, ) } Column(modifier = Modifier.weight(1f)) { + Text( + text = card.ownerLogin, + style = MaterialTheme.typography.labelSmall.copy( + fontSize = 11.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Text( text = card.name, style = MaterialTheme.typography.titleMedium.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(2.dp)) - StarTier(stars = card.starsCount) } Row( modifier = Modifier .clip(RoundedCornerShape(50)) - .background(card.freshnessColor.copy(alpha = 0.18f)) + .background(EmberPalette.Deep) .padding(horizontal = 8.dp, vertical = 3.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), ) { - Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(card.freshnessColor)) + Icon( + imageVector = Icons.Default.LocalFireDepartment, + contentDescription = null, + modifier = Modifier.size(11.dp), + tint = Color.White.copy(alpha = flamePulse), + ) Text( text = card.relativeAgoLabel, - color = card.freshnessColor, + color = Color.White, style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontSize = 10.sp, ), ) } } - Spacer(Modifier.height(8.dp)) - Text( - text = card.description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.height(36.dp), - ) - Spacer(Modifier.height(10.dp)) - HorizontalDivider( - color = MaterialTheme.colorScheme.outlineVariant, - thickness = 0.5.dp, - ) - Spacer(Modifier.height(8.dp)) + if (card.description.isNotBlank()) { + Spacer(Modifier.height(8.dp)) + Text( + text = card.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(Modifier.weight(1f)) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - card.topics.take(2).forEach { topic -> - TopicGlyph(topic = topic, sizeDp = 14) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + Icon( + imageVector = Icons.Outlined.Star, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = formatCount(card.starsCount), + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) } - Box(Modifier.weight(1f)) - card.platforms.forEach { kind -> - PlatformGlyph(kind = kind, supported = true, sizeDp = 14) + if (card.downloadsCount > 0) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = formatCount(card.downloadsCount), + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } + if (card.topics.isNotEmpty() || card.platforms.isNotEmpty()) { + Spacer(Modifier.height(8.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 0.5.dp, + ) + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + card.topics.take(2).forEach { topic -> + TopicGlyph(topic = topic, sizeDp = 14) + } + Box(Modifier.weight(1f)) + card.platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = 14) + } } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 57ce8863b..934a0c53c 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -2,6 +2,7 @@ package zed.rainxch.home.presentation.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -9,33 +10,52 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.buttons.PrimaryButton -import zed.rainxch.core.presentation.components.cards.LeadHeroCard +import zed.rainxch.core.presentation.theme.shapes.CornerRadii import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape -import zed.rainxch.core.presentation.vocabulary.AppAccent -import zed.rainxch.core.presentation.vocabulary.FreshnessRing -import zed.rainxch.core.presentation.vocabulary.StarTier -import zed.rainxch.core.presentation.vocabulary.TopicGlyph +import zed.rainxch.core.presentation.theme.tokens.EmberPalette +import zed.rainxch.core.presentation.utils.formatCount +import zed.rainxch.core.presentation.vocabulary.PlatformGlyph +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_action_get +import zed.rainxch.githubstore.core.presentation.res.open +import zed.rainxch.githubstore.core.presentation.res.update import zed.rainxch.home.presentation.model.HomeRepoCardUi +private val LeadShape = WonkySquircleShape( + topStart = CornerRadii(32.dp, 26.dp), + topEnd = CornerRadii(26.dp, 32.dp), + bottomEnd = CornerRadii(32.dp, 26.dp), + bottomStart = CornerRadii(26.dp, 32.dp), +) + @OptIn(ExperimentalFoundationApi::class) @Composable fun LeadCard( @@ -45,113 +65,213 @@ fun LeadCard( modifier: Modifier = Modifier, ) { val isDark = isSystemInDarkTheme() - Column(modifier = modifier.fillMaxWidth()) { - Row( - modifier = Modifier - .clip(RoundedCornerShape(50)) - .background(card.freshnessColor.copy(alpha = 0.18f)) - .padding(horizontal = 12.dp, vertical = 5.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(6.dp) - .clip(CircleShape) - .background(card.freshnessColor), - ) - Text( - text = "HOT · ${card.relativeAgoLabel} ago", - color = card.freshnessColor, - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - ), - ) - } - Spacer(Modifier.height(8.dp)) - LeadHeroCard( - accent = AppAccent( - c = card.accentSaturated, - lt = card.accentLightTint, - dtAlpha = card.accentDarkAlpha, - ), - isDark = isDark, - modifier = Modifier - .clip(WonkySquircleShape.CtaPrimary) - .combinedClickable(onClick = onClick, onLongClick = onLongClick), - ) { + val surface = MaterialTheme.colorScheme.surface + val borderColor = if (isDark) EmberPalette.Hot.copy(alpha = 0.42f) else EmberPalette.Deep.copy(alpha = 0.5f) + + Box( + modifier = modifier + .fillMaxWidth() + .height(248.dp) + .clip(LeadShape) + .background(if (isDark) EmberPalette.Ash else surface) + .drawBehind { + val warmth = Brush.linearGradient( + colorStops = arrayOf( + 0f to EmberPalette.Hot.copy(alpha = if (isDark) 0.32f else 0.16f), + 0.6f to EmberPalette.Warm.copy(alpha = if (isDark) 0.18f else 0.08f), + 1f to Color.Transparent, + ), + start = Offset(0f, 0f), + end = Offset(size.width * 0.85f, size.height), + ) + drawRect(brush = warmth) + val sun = Brush.radialGradient( + colors = listOf( + EmberPalette.Amber.copy(alpha = if (isDark) 0.18f else 0.12f), + Color.Transparent, + ), + center = Offset(size.width * 0.18f, size.height * 0.25f), + radius = size.minDimension * 0.7f, + ) + drawRect(brush = sun) + } + .border(width = 1.5.dp, color = borderColor, shape = LeadShape) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding(horizontal = 18.dp, vertical = 18.dp), + ) { + Column(modifier = Modifier.fillMaxSize()) { Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(14.dp), ) { - FreshnessRing( - sizeDp = 80, - color = card.accentSaturated, - daysSinceRelease = card.daysSinceUpdate, + Box( + modifier = Modifier + .size(76.dp) + .clip(CircleShape) + .background(if (isDark) EmberPalette.Ash else surface) + .border(2.5.dp, EmberPalette.Deep, CircleShape), + contentAlignment = Alignment.Center, ) { GitHubStoreImage( imageModel = { card.ownerAvatarUrl }, - modifier = Modifier.size(80.dp).clip(CircleShape), + modifier = Modifier.size(68.dp).clip(CircleShape), + extractDominantFor = card.ownerAvatarUrl, ) } Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = card.ownerLogin, + style = MaterialTheme.typography.labelMedium.copy( + fontSize = 12.sp, + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Spacer(Modifier.size(6.dp)) + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(EmberPalette.Deep) + .padding(horizontal = 10.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "HOT", + color = Color.White, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Black, + fontSize = 9.5.sp, + letterSpacing = 0.8.sp, + ), + ) + Text( + text = "·", + color = Color.White.copy(alpha = 0.7f), + style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp), + ) + Text( + text = card.relativeAgoLabel, + color = Color.White, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 10.sp, + ), + ) + } + } Text( text = card.name, - style = MaterialTheme.typography.headlineSmall.copy( - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.SemiBold, - fontSize = 26.sp, + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Black, + fontSize = 28.sp, + letterSpacing = (-0.3).sp, ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(2.dp)) - Text( - text = card.ownerLogin, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Spacer(Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - StarTier(stars = card.starsCount) - card.topics.take(3).forEach { topic -> - TopicGlyph(topic = topic, sizeDp = 14) - } - } } } - Spacer(Modifier.height(14.dp)) + Spacer(Modifier.height(10.dp)) if (card.description.isNotBlank()) { Text( text = card.description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 13.sp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.78f), maxLines = 2, overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(14.dp)) + Spacer(Modifier.height(10.dp)) } Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), ) { - PrimaryButton(onClick = onClick) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = Icons.Outlined.Star, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + modifier = Modifier.size(15.dp), + ) Text( - text = when { - card.isUpdateAvailable -> "Update" - card.isInstalled -> "Open" - else -> "Get" - }, + text = formatCount(card.starsCount), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), ) } + if (card.downloadsCount > 0) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + modifier = Modifier.size(15.dp), + ) + Text( + text = formatCount(card.downloadsCount), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + ) + } + } + Box(modifier = Modifier.weight(1f)) + if (card.platforms.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + card.platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = 15) + } + } + } + } + Spacer(Modifier.weight(1f)) + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(50)) + .background(EmberPalette.Deep) + .padding(vertical = 13.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource( + when { + card.isUpdateAvailable -> Res.string.update + card.isInstalled -> Res.string.open + else -> Res.string.home_action_get + }, + ), + color = Color.White, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + fontSize = 17.sp, + ), + ) } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/SeeAllTile.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/SeeAllTile.kt new file mode 100644 index 000000000..2744acb9b --- /dev/null +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/SeeAllTile.kt @@ -0,0 +1,129 @@ +package zed.rainxch.home.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.shapes.CornerRadii +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_view_all +import zed.rainxch.githubstore.core.presentation.res.home_view_more + +private val SeeAllHotShape = WonkySquircleShape( + topStart = CornerRadii(24.dp, 18.dp), + topEnd = CornerRadii(18.dp, 24.dp), + bottomEnd = CornerRadii(24.dp, 18.dp), + bottomStart = CornerRadii(18.dp, 24.dp), +) + +@Composable +fun SeeAllHotTile( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(width = 130.dp, height = 186.dp) + .clip(SeeAllHotShape) + .background(MaterialTheme.colorScheme.surface) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = SeeAllHotShape) + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 14.dp, vertical = 18.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(androidx.compose.foundation.shape.CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp), + ) + } + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(Res.string.home_view_all), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +fun SeeMoreRow( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val borderColor = MaterialTheme.colorScheme.outline + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .clip(androidx.compose.foundation.shape.RoundedCornerShape(28.dp)) + .border( + width = 1.dp, + color = borderColor, + shape = androidx.compose.foundation.shape.RoundedCornerShape(28.dp), + ) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(Res.string.home_view_more), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } +} diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt index bb9c5e8c4..182c237a4 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt @@ -24,9 +24,15 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_action_get +import zed.rainxch.githubstore.core.presentation.res.open +import zed.rainxch.githubstore.core.presentation.res.update import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.buttons.OutlineButton import zed.rainxch.core.presentation.components.cards.RowCard +import zed.rainxch.core.presentation.vocabulary.PlatformGlyph import zed.rainxch.core.presentation.vocabulary.StarTier import zed.rainxch.home.presentation.model.HomeRepoCardUi @@ -62,7 +68,6 @@ fun StarredRowItem( Text( text = card.name, style = MaterialTheme.typography.titleSmall.copy( - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, @@ -82,13 +87,25 @@ fun StarredRowItem( StarTier(stars = card.starsCount, size = 10) } } + if (card.platforms.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + card.platforms.forEach { kind -> + PlatformGlyph(kind = kind, supported = true, sizeDp = 13) + } + } + } OutlineButton(onClick = onClick) { Text( - text = when { - card.isUpdateAvailable -> "Update" - card.isInstalled -> "Open" - else -> "Get" - }, + text = stringResource( + when { + card.isUpdateAvailable -> Res.string.update + card.isInstalled -> Res.string.open + else -> Res.string.home_action_get + }, + ), ) } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt index 5eb1993c2..ac8c1ce1a 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt @@ -1,15 +1,17 @@ package zed.rainxch.home.presentation.components -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.outlined.AccountTree +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,17 +21,23 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.color.avatarColorFor import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.buttons.OutlineButton -import zed.rainxch.core.presentation.components.cards.RowCard -import zed.rainxch.core.presentation.vocabulary.PlatformGlyph -import zed.rainxch.core.presentation.vocabulary.StarTier +import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.components.cards.RepoStripeCard +import zed.rainxch.core.presentation.components.chips.PlatformsChip +import zed.rainxch.core.presentation.components.chips.StatChip +import zed.rainxch.core.presentation.utils.formatCount +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.home_action_get +import zed.rainxch.githubstore.core.presentation.res.home_rank_format +import zed.rainxch.githubstore.core.presentation.res.open +import zed.rainxch.githubstore.core.presentation.res.update import zed.rainxch.home.presentation.model.HomeRepoCardUi -@OptIn(ExperimentalFoundationApi::class) @Composable fun TrendingRowItem( card: HomeRepoCardUi, @@ -41,14 +49,13 @@ fun TrendingRowItem( RankRowItem( card = card, rank = rank, - rankColor = MaterialTheme.colorScheme.primary, + rankColor = MaterialTheme.colorScheme.onSurface, onClick = onClick, onLongClick = onLongClick, modifier = modifier, ) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun PopularRowItem( card: HomeRepoCardUi, @@ -60,14 +67,14 @@ fun PopularRowItem( RankRowItem( card = card, rank = rank, - rankColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f), + rankColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f), onClick = onClick, onLongClick = onLongClick, modifier = modifier, ) } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable private fun RankRowItem( card: HomeRepoCardUi, @@ -77,68 +84,108 @@ private fun RankRowItem( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier - .fillMaxWidth() - .combinedClickable(onClick = onClick, onLongClick = onLongClick), - ) { - RowCard { + val accent = avatarColorFor(card.ownerAvatarUrl, MaterialTheme.colorScheme.primary) + RepoStripeCard( + accent = accent, + ownerLogin = card.ownerLogin, + name = card.name, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + stripeTrailing = { Text( - text = "#$rank", + text = stringResource(Res.string.home_rank_format, rank), color = rankColor, - style = MaterialTheme.typography.titleMedium.copy( - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, + style = MaterialTheme.typography.displaySmall.copy( + fontWeight = FontWeight.Black, + fontSize = 34.sp, + letterSpacing = (-0.5).sp, ), - modifier = Modifier.width(38.dp), ) + }, + avatar = { GitHubStoreImage( imageModel = { card.ownerAvatarUrl }, - modifier = Modifier.size(36.dp).clip(CircleShape), + modifier = Modifier.size(72.dp).clip(CircleShape), + extractDominantFor = card.ownerAvatarUrl, ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = card.name, - style = MaterialTheme.typography.titleSmall.copy( - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + }, + chips = { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (card.starsCount > 0) { + StatChip( + label = formatCount(card.starsCount), + leading = { + Icon( + imageVector = Icons.Outlined.Star, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + if (card.rawRepository.forksCount > 0) { + StatChip( + label = formatCount(card.rawRepository.forksCount), + leading = { + Icon( + imageVector = Icons.Outlined.AccountTree, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + if (card.downloadsCount > 0) { + StatChip( + label = formatCount(card.downloadsCount), + leading = { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + if (card.platforms.isNotEmpty()) { + PlatformsChip(platforms = card.platforms) + } + } + }, + languagePill = null, + cta = { + PrimaryButton( + onClick = onClick, + backgroundColor = MaterialTheme.colorScheme.onSurface, + contentColor = MaterialTheme.colorScheme.surface, + ) { Row( - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, ) { - StarTier(stars = card.starsCount, size = 10) Text( - text = card.ownerLogin, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = stringResource( + when { + card.isUpdateAvailable -> Res.string.update + card.isInstalled -> Res.string.open + else -> Res.string.home_action_get + }, + ), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + modifier = Modifier.size(16.dp), ) } } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - card.platforms.forEach { kind -> - PlatformGlyph(kind = kind, supported = true, sizeDp = 12) - } - } - OutlineButton(onClick = onClick) { - Text( - text = when { - card.isUpdateAvailable -> "Update" - card.isInstalled -> "Open" - else -> "Get" - }, - ) - } - } - } + }, + ) } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt index e28ee19c3..7576cbda0 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardMapper.kt @@ -35,6 +35,8 @@ fun toHomeRepoCardUi( ownerAvatarUrl = ui.owner.avatarUrl, description = ui.description.orEmpty(), starsCount = ui.stargazersCount, + downloadsCount = repo.downloadCount, + language = repo.language, daysSinceUpdate = days, relativeAgoLabel = relativeAgo(repo.updatedAt), freshnessState = freshness.state, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt index 41086efa4..71b9f1317 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/model/HomeRepoCardUi.kt @@ -15,6 +15,8 @@ data class HomeRepoCardUi( val ownerAvatarUrl: String, val description: String, val starsCount: Int, + val downloadsCount: Long, + val language: String?, val daysSinceUpdate: Int, val relativeAgoLabel: String, val freshnessState: FreshnessState, From 5c4248648dd38264fab6cc2011d9c8248864d7b6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:18 +0500 Subject: [PATCH 043/172] feat(apps): floating topbar, Apple dropdowns, outlined rows --- .../AdvancedAppSettingsBottomSheet.kt | 11 +- .../components/AppsSectionHeader.kt | 98 +++++++++------- .../presentation/components/CompactAppRow.kt | 105 +++++++++--------- .../components/ImportSummarySheet.kt | 8 +- .../components/LinkAppBottomSheet.kt | 8 +- .../components/VariantPickerDialog.kt | 1 + .../presentation/import/ExternalImportRoot.kt | 10 +- 7 files changed, 123 insertions(+), 118 deletions(-) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt index 4b477a235..81c1f8fea 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt @@ -30,7 +30,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch @@ -58,17 +58,14 @@ fun AdvancedAppSettingsBottomSheet( onAction: (AppsAction) -> Unit, ) { val app = state.advancedSettingsApp ?: return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = { onAction(AppsAction.OnDismissAdvancedSettings) }, - sheetState = sheetState, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 24.dp), + .fillMaxWidth(), ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt index 89c79a260..94545bf9d 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppsSectionHeader.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -26,7 +27,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.apps_section_collapse import zed.rainxch.githubstore.core.presentation.res.apps_section_count_suffix @@ -55,54 +58,63 @@ fun AppsSectionHeader( val rowSemantic = if (isExpanded) collapseLabel else expandLabel - Row( - modifier = - modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(top = 16.dp, bottom = 4.dp) - .let { base -> - if (collapsible) { - base - .clickable(onClick = onToggle) - .semantics(mergeDescendants = true) { - role = Role.Button - heading() - contentDescription = "$title, $count, $rowSemantic" - stateDescription = if (isExpanded) expandedStateLabel else collapsedStateLabel - } - } else { - base.semantics(mergeDescendants = true) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 20.dp, bottom = 6.dp) + .let { base -> + if (collapsible) { + base + .clickable(onClick = onToggle) + .semantics(mergeDescendants = true) { + role = Role.Button heading() - contentDescription = "$title, $count" + contentDescription = "$title, $count, $rowSemantic" + stateDescription = if (isExpanded) expandedStateLabel else collapsedStateLabel } + } else { + base.semantics(mergeDescendants = true) { + heading() + contentDescription = "$title, $count" } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), + } + }, + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.apps_section_count_suffix, count), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.weight(1f), - ) - if (collapsible) { - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = - Modifier - .size(20.dp) - .rotate(rotation), + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource(Res.string.apps_section_count_suffix, count), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + fontSize = 13.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), ) + if (collapsible) { + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(22.dp) + .rotate(rotation), + ) + } } + Squiggle() } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt index 115b88384..c336c56a6 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt @@ -3,7 +3,9 @@ package zed.rainxch.apps.presentation.components import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import zed.rainxch.core.presentation.theme.tokens.Radii import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,8 +26,8 @@ import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -95,16 +97,21 @@ fun CompactAppRow( val rowSemanticName = buildCompactRowSemantics(app.appName, app.installedVersion, flags) Row( - modifier = - modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 64.dp) - .clip(RoundedCornerShape(16.dp)) - .clickable(onClick = onRowClick) - .padding(horizontal = 12.dp, vertical = 8.dp) - .semantics(mergeDescendants = true) { - contentDescription = rowSemanticName - }, + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 68.dp) + .clip(Radii.row) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = Radii.row, + ) + .background(MaterialTheme.colorScheme.surface) + .clickable(onClick = onRowClick) + .padding(horizontal = 14.dp, vertical = 12.dp) + .semantics(mergeDescendants = true) { + contentDescription = rowSemanticName + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -241,47 +248,47 @@ private fun CompactRowOverflow( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - DropdownMenu( + GhsDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.advanced_settings_open)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.advanced_settings_open), onClick = { expanded = false onAdvancedSettingsClick() }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.variant_picker_open)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.variant_picker_open), onClick = { expanded = false onPickVariantClick() }, ) - DropdownMenuItem( - text = { - val baseLabel = stringResource(Res.string.pre_release_badge) - Text(text = if (isPreReleaseEnabled) "$baseLabel ✓" else baseLabel) - }, - onClick = { - expanded = false - onTogglePreReleases(!isPreReleaseEnabled) - }, - ) - DropdownMenuItem( - text = { - val baseLabel = stringResource(Res.string.apps_ignore_updates) - Text(text = if (!isUpdateCheckEnabled) "$baseLabel ✓" else baseLabel) - }, - onClick = { - expanded = false - onToggleUpdateCheck(!isUpdateCheckEnabled) - }, - ) + run { + val baseLabel = stringResource(Res.string.pre_release_badge) + GhsDropdownMenuItem( + text = if (isPreReleaseEnabled) "$baseLabel ✓" else baseLabel, + onClick = { + expanded = false + onTogglePreReleases(!isPreReleaseEnabled) + }, + ) + } + run { + val baseLabel = stringResource(Res.string.apps_ignore_updates) + GhsDropdownMenuItem( + text = if (!isUpdateCheckEnabled) "$baseLabel ✓" else baseLabel, + onClick = { + expanded = false + onToggleUpdateCheck(!isUpdateCheckEnabled) + }, + ) + } if (hasSkippedReleaseTag) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.apps_skip_version_unskip)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.apps_skip_version_unskip), onClick = { expanded = false onUnskipVersionClick() @@ -289,13 +296,9 @@ private fun CompactRowOverflow( ) } if (isPending) { - DropdownMenuItem( - text = { - Text( - text = stringResource(Res.string.discard_pending_install), - color = MaterialTheme.colorScheme.error, - ) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.discard_pending_install), + contentColor = MaterialTheme.colorScheme.error, leadingIcon = { Icon( imageVector = Icons.Outlined.DeleteOutline, @@ -309,13 +312,9 @@ private fun CompactRowOverflow( }, ) } else { - DropdownMenuItem( - text = { - Text( - text = stringResource(Res.string.uninstall), - color = MaterialTheme.colorScheme.error, - ) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.uninstall), + contentColor = MaterialTheme.colorScheme.error, leadingIcon = { Icon( imageVector = Icons.Outlined.DeleteOutline, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt index 025e90f56..524df7d46 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt @@ -26,7 +26,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -80,10 +80,9 @@ fun ImportSummarySheet( return } - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), ) { Column( modifier = Modifier @@ -185,10 +184,9 @@ private fun UnknownFormatSheet( sheetState: androidx.compose.material3.SheetState, onDismiss: () -> Unit, ) { - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), ) { Column( modifier = Modifier diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt index 7fac63027..c8be8afb1 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt @@ -33,7 +33,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -67,11 +67,9 @@ fun LinkAppBottomSheet( state: AppsState, onAction: (AppsAction) -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = { onAction(AppsAction.OnDismissLinkSheet) }, - sheetState = sheetState, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { AnimatedContent( targetState = state.linkStep, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt index 339706602..131bd5dcc 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt @@ -50,6 +50,7 @@ fun VariantPickerDialog( AlertDialog( onDismissRequest = { onAction(AppsAction.OnDismissVariantPicker) }, + shape = zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape.Dialog, title = { Column { Text( diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt index 27cb553ad..81878200a 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -129,12 +129,12 @@ fun ExternalImportRoot( contentDescription = stringResource(Res.string.external_import_overflow_more), ) } - DropdownMenu( + GhsDropdownMenu( expanded = menuOpen, onDismissRequest = { menuOpen = false }, ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.external_import_overflow_skip_remaining)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.external_import_overflow_skip_remaining), onClick = { menuOpen = false viewModel.onAction(ExternalImportAction.OnSkipRemaining) From 2efc645c151ccee16c3df24e500dcd5db6f1015f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:24 +0500 Subject: [PATCH 044/172] feat(apps): updates banner and ready-to-install section --- .../rainxch/apps/presentation/AppsAction.kt | 6 + .../zed/rainxch/apps/presentation/AppsRoot.kt | 377 ++++++----- .../rainxch/apps/presentation/AppsState.kt | 3 + .../apps/presentation/AppsViewModel.kt | 8 + .../presentation/components/AppDetailPane.kt | 620 ++++++++++++++++++ .../presentation/components/UpdatesBanner.kt | 247 +++++++ 6 files changed, 1088 insertions(+), 173 deletions(-) create mode 100644 feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt create mode 100644 feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt index 3a938496b..f8f5e4929 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt @@ -40,6 +40,12 @@ sealed interface AppsAction { data object OnToggleUpToDateSection : AppsAction + data object OnToggleUpdatesSection : AppsAction + + data class OnTwoPaneSelect( + val packageName: String?, + ) : AppsAction + data class OnNavigateToRepo( val repoId: Long, val sourceHost: String? = null, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index 73041b5b0..b3042a76d 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -2,8 +2,12 @@ package zed.rainxch.apps.presentation +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.ui.unit.sp import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -42,12 +46,11 @@ import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton @@ -99,6 +102,7 @@ import zed.rainxch.apps.presentation.components.InstalledAppIcon import zed.rainxch.apps.presentation.components.LinkAppBottomSheet import zed.rainxch.apps.presentation.components.VariantPickerDialog import zed.rainxch.apps.presentation.components.KaoBanner +import zed.rainxch.apps.presentation.components.UpdatesBanner import zed.rainxch.apps.presentation.import.components.ImportProposalBanner import zed.rainxch.apps.presentation.model.AppItem import zed.rainxch.apps.presentation.model.AppSortRule @@ -119,8 +123,8 @@ import zed.rainxch.githubstore.core.presentation.res.apps_compact_more_actions import zed.rainxch.githubstore.core.presentation.res.apps_ignore_updates import zed.rainxch.githubstore.core.presentation.res.apps_skip_version import zed.rainxch.githubstore.core.presentation.res.apps_skip_version_unskip +import zed.rainxch.githubstore.core.presentation.res.apps_section_pending_installs import zed.rainxch.githubstore.core.presentation.res.apps_section_up_to_date -import zed.rainxch.githubstore.core.presentation.res.apps_section_updates_available import zed.rainxch.githubstore.core.presentation.res.install import zed.rainxch.githubstore.core.presentation.res.ready_to_install import zed.rainxch.githubstore.core.presentation.res.variant_label_inline @@ -132,13 +136,13 @@ import zed.rainxch.githubstore.core.presentation.res.checking import zed.rainxch.githubstore.core.presentation.res.checking_for_updates import zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_message import zed.rainxch.githubstore.core.presentation.res.confirm_uninstall_title -import zed.rainxch.githubstore.core.presentation.res.currently_updating import zed.rainxch.githubstore.core.presentation.res.downloading import zed.rainxch.githubstore.core.presentation.res.error_with_message import zed.rainxch.githubstore.core.presentation.res.export_apps import zed.rainxch.githubstore.core.presentation.res.export_apps_obtainium import zed.rainxch.githubstore.core.presentation.res.external_import_rescan_menu import zed.rainxch.githubstore.core.presentation.res.import_apps +import zed.rainxch.githubstore.core.presentation.res.bottom_nav_apps_title import zed.rainxch.githubstore.core.presentation.res.installed_apps import zed.rainxch.githubstore.core.presentation.res.installing import zed.rainxch.githubstore.core.presentation.res.last_checked @@ -159,9 +163,7 @@ import zed.rainxch.githubstore.core.presentation.res.confirm_discard_pending_mes import zed.rainxch.githubstore.core.presentation.res.confirm_discard_pending_title import zed.rainxch.githubstore.core.presentation.res.discard_pending_install import zed.rainxch.githubstore.core.presentation.res.update -import zed.rainxch.githubstore.core.presentation.res.update_all import zed.rainxch.githubstore.core.presentation.res.updated_successfully -import zed.rainxch.githubstore.core.presentation.res.updating_x_of_y @Composable fun AppsRoot( @@ -250,43 +252,68 @@ fun AppsScreen( Scaffold( topBar = { - TopAppBar( - title = { - Text( - text = stringResource(Res.string.installed_apps), - style = MaterialTheme.typography.titleMediumEmphasized, + Row( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(Res.string.bottom_nav_apps_title), + style = MaterialTheme.typography.headlineSmall.copy( fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - }, - actions = { + fontSize = 24.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp), + ) + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(50), + ), + verticalAlignment = Alignment.CenterVertically, + ) { Box { - IconButton(onClick = { showSortMenu = true }) { + Box( + modifier = Modifier + .clickable { showSortMenu = true } + .padding(horizontal = 12.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { Icon( imageVector = Icons.AutoMirrored.Filled.Sort, contentDescription = stringResource(Res.string.sort_apps), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(18.dp), ) } - DropdownMenu( + GhsDropdownMenu( expanded = showSortMenu, onDismissRequest = { showSortMenu = false }, ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.sort_updates_first)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.sort_updates_first), onClick = { showSortMenu = false onAction(AppsAction.OnSortRuleSelected(AppSortRule.UpdatesFirst)) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.sort_recently_updated)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.sort_recently_updated), onClick = { showSortMenu = false onAction(AppsAction.OnSortRuleSelected(AppSortRule.RecentlyUpdated)) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.sort_name)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.sort_name), onClick = { showSortMenu = false onAction(AppsAction.OnSortRuleSelected(AppSortRule.Name)) @@ -295,28 +322,49 @@ fun AppsScreen( } } - IconButton( - onClick = { onAction(AppsAction.OnCheckAllForUpdates) }, + Box( + modifier = Modifier + .size(1.dp, 20.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + ) + Box( + modifier = Modifier + .clickable { onAction(AppsAction.OnCheckAllForUpdates) } + .padding(horizontal = 12.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, ) { Icon( imageVector = Icons.Default.Refresh, contentDescription = stringResource(Res.string.check_for_updates), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(18.dp), ) } - + Box( + modifier = Modifier + .size(1.dp, 20.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + ) Box { - IconButton(onClick = { showOverflowMenu = true }) { + Box( + modifier = Modifier + .clickable { showOverflowMenu = true } + .padding(horizontal = 12.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { Icon( imageVector = Icons.Outlined.MoreVert, contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(18.dp), ) } - DropdownMenu( + GhsDropdownMenu( expanded = showOverflowMenu, onDismissRequest = { showOverflowMenu = false }, ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.export_apps)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.export_apps), onClick = { showOverflowMenu = false onAction(AppsAction.OnExportApps) @@ -325,8 +373,8 @@ fun AppsScreen( Icon(Icons.Outlined.FileUpload, contentDescription = null) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.export_apps_obtainium)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.export_apps_obtainium), onClick = { showOverflowMenu = false onAction(AppsAction.OnExportObtainium) @@ -335,8 +383,8 @@ fun AppsScreen( Icon(Icons.Outlined.FileUpload, contentDescription = null) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.import_apps)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.import_apps), onClick = { showOverflowMenu = false onAction(AppsAction.OnImportApps) @@ -345,8 +393,8 @@ fun AppsScreen( Icon(Icons.Outlined.FileDownload, contentDescription = null) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.external_import_rescan_menu)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.external_import_rescan_menu), onClick = { showOverflowMenu = false onAction(AppsAction.OnRescanForGithubApps) @@ -355,8 +403,8 @@ fun AppsScreen( Icon(Icons.Outlined.Search, contentDescription = null) }, ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.add_from_starred_title)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.add_from_starred_title), onClick = { showOverflowMenu = false onAction(AppsAction.OnAddFromStarredClick) @@ -370,8 +418,8 @@ fun AppsScreen( ) } } - }, - ) + } + } }, floatingActionButton = { ExtendedFloatingActionButton( @@ -558,41 +606,6 @@ fun AppsScreen( ) } - val hasUpdates = - state.apps.any { - it.installedApp.isUpdateAvailable && it.installedApp.updateCheckEnabled - } - if (hasUpdates && !state.isUpdatingAll) { - Button( - onClick = { onAction(AppsAction.OnUpdateAll) }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - enabled = state.updateAllButtonEnabled, - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - ) - - Spacer(Modifier.width(8.dp)) - - Text( - text = stringResource(Res.string.update_all), - ) - } - } - - if (state.isUpdatingAll && state.updateAllProgress != null) { - UpdateAllProgressCard( - progress = state.updateAllProgress, - onCancel = { - onAction(AppsAction.OnCancelUpdateAll) - }, - ) - } - when { state.isLoading -> { Box( @@ -620,13 +633,33 @@ fun AppsScreen( val listState = rememberLazyListState() val isScrollbarEnabled = LocalScrollbarEnabled.current + val pendingGroup = + state.filteredApps.filter { + it.installedApp.isPendingInstall && + it.installedApp.pendingInstallFilePath != null + } val updatesGroup = state.filteredApps.filter { - it.installedApp.isUpdateAvailable && it.installedApp.updateCheckEnabled + it.installedApp.isUpdateAvailable && + it.installedApp.updateCheckEnabled && + !it.installedApp.isPendingInstall } val idleGroup = state.filteredApps.filter { - !it.installedApp.isUpdateAvailable || !it.installedApp.updateCheckEnabled + (!it.installedApp.isUpdateAvailable || !it.installedApp.updateCheckEnabled) && + !it.installedApp.isPendingInstall + } + + val onRowSelect: (zed.rainxch.apps.presentation.model.InstalledAppUi) -> Unit = + { app -> + onAction( + AppsAction.OnNavigateToRepo( + repoId = app.repoId, + sourceHost = app.sourceHost, + owner = app.repoOwner, + repo = app.repoName, + ), + ) } ScrollbarContainer( @@ -669,19 +702,19 @@ fun AppsScreen( } } - if (updatesGroup.isNotEmpty()) { - item(key = "header-updates-available") { + if (pendingGroup.isNotEmpty()) { + item(key = "header-pending-installs") { AppsSectionHeader( - title = stringResource(Res.string.apps_section_updates_available), - count = updatesGroup.size, + title = stringResource(Res.string.apps_section_pending_installs), + count = pendingGroup.size, isExpanded = true, collapsible = false, onToggle = {}, ) } items( - items = updatesGroup, - key = { "rich-${it.installedApp.packageName}" }, + items = pendingGroup, + key = { "pending-${it.installedApp.packageName}" }, ) { appItem -> Box(modifier = Modifier.padding(horizontal = 16.dp)) { AppItemCard( @@ -690,16 +723,81 @@ fun AppsScreen( onUpdateClick = { onAction(AppsAction.OnUpdateApp(appItem.installedApp)) }, onCancelClick = { onAction(AppsAction.OnCancelUpdate(appItem.installedApp.packageName)) }, onUninstallClick = { onAction(AppsAction.OnUninstallApp(appItem.installedApp)) }, - onRepoClick = { + onRepoClick = { onRowSelect(appItem.installedApp) }, + onTogglePreReleases = { enabled -> + onAction(AppsAction.OnTogglePreReleases(appItem.installedApp.packageName, enabled)) + }, + onToggleUpdateCheck = { enabled -> + onAction(AppsAction.OnToggleUpdateCheck(appItem.installedApp.packageName, enabled)) + }, + onAdvancedSettingsClick = { + onAction(AppsAction.OnOpenAdvancedSettings(appItem.installedApp)) + }, + onPickVariantClick = { onAction( - AppsAction.OnNavigateToRepo( - repoId = appItem.installedApp.repoId, - sourceHost = appItem.installedApp.sourceHost, - owner = appItem.installedApp.repoOwner, - repo = appItem.installedApp.repoName, + AppsAction.OnOpenVariantPicker( + app = appItem.installedApp, + resumeUpdateAfterPick = false, ), ) }, + onInstallPendingClick = { + onAction(AppsAction.OnInstallPendingApp(appItem.installedApp)) + }, + onDiscardPendingClick = { + onAction(AppsAction.OnDiscardPendingInstall(appItem.installedApp)) + }, + onSkipVersionClick = { + val tag = + appItem.installedApp.latestVersion + ?: appItem.installedApp.latestVersionName + if (!tag.isNullOrBlank()) { + onAction( + AppsAction.OnSkipReleaseTag( + appItem.installedApp.packageName, + tag, + ), + ) + } + }, + onUnskipVersionClick = { + onAction(AppsAction.OnUnskipReleaseTag(appItem.installedApp.packageName)) + }, + ) + } + } + } + + if (updatesGroup.isNotEmpty() || state.isUpdatingAll) { + item(key = "updates-banner") { + Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) { + UpdatesBanner( + count = updatesGroup.size, + isExpanded = state.isUpdatesSectionExpanded, + isUpdatingAll = state.isUpdatingAll, + updateAllProgress = state.updateAllProgress, + updateAllEnabled = state.updateAllButtonEnabled, + onUpdateAll = { onAction(AppsAction.OnUpdateAll) }, + onCancelUpdateAll = { onAction(AppsAction.OnCancelUpdateAll) }, + onToggleExpanded = { onAction(AppsAction.OnToggleUpdatesSection) }, + ) + } + } + } + + if (updatesGroup.isNotEmpty() && state.isUpdatesSectionExpanded) { + items( + items = updatesGroup, + key = { "rich-${it.installedApp.packageName}" }, + ) { appItem -> + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + AppItemCard( + appItem = appItem, + onOpenClick = { onAction(AppsAction.OnOpenApp(appItem.installedApp)) }, + onUpdateClick = { onAction(AppsAction.OnUpdateApp(appItem.installedApp)) }, + onCancelClick = { onAction(AppsAction.OnCancelUpdate(appItem.installedApp.packageName)) }, + onUninstallClick = { onAction(AppsAction.OnUninstallApp(appItem.installedApp)) }, + onRepoClick = { onRowSelect(appItem.installedApp) }, onTogglePreReleases = { enabled -> onAction(AppsAction.OnTogglePreReleases(appItem.installedApp.packageName, enabled)) }, @@ -808,16 +906,7 @@ fun AppsScreen( ), ) }, - onRowClick = { - onAction( - AppsAction.OnNavigateToRepo( - repoId = appItem.installedApp.repoId, - sourceHost = appItem.installedApp.sourceHost, - owner = appItem.installedApp.repoOwner, - repo = appItem.installedApp.repoName, - ), - ) - }, + onRowClick = { onRowSelect(appItem.installedApp) }, ) } } @@ -836,64 +925,6 @@ fun AppsScreen( } } -@Composable -fun UpdateAllProgressCard( - progress: UpdateAllProgress, - onCancel: () -> Unit, -) { - Card( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Column( - modifier = Modifier.padding(16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = - stringResource( - Res.string.updating_x_of_y, - progress.current, - progress.total, - ), - style = MaterialTheme.typography.titleMedium, - ) - IconButton(onClick = onCancel) { - Icon( - Icons.Default.Close, - contentDescription = stringResource(Res.string.cancel), - ) - } - } - - Spacer(Modifier.height(8.dp)) - - Text( - text = - stringResource( - Res.string.currently_updating, - progress.currentAppName, - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Spacer(Modifier.height(12.dp)) - - LinearWavyProgressIndicator( - progress = { progress.current.toFloat() / progress.total }, - modifier = Modifier.fillMaxWidth(), - ) - } - } -} - @Composable fun AppItemCard( appItem: AppItem, @@ -1171,32 +1202,32 @@ fun AppItemCard( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - DropdownMenu( + GhsDropdownMenu( expanded = showRowOverflow, onDismissRequest = { showRowOverflow = false }, ) { - DropdownMenuItem( - text = { - val baseLabel = stringResource(Res.string.apps_ignore_updates) - Text(text = if (!app.updateCheckEnabled) "$baseLabel ✓" else baseLabel) - }, - onClick = { - showRowOverflow = false - onToggleUpdateCheck(!app.updateCheckEnabled) - }, - ) + run { + val baseLabel = stringResource(Res.string.apps_ignore_updates) + GhsDropdownMenuItem( + text = if (!app.updateCheckEnabled) "$baseLabel ✓" else baseLabel, + onClick = { + showRowOverflow = false + onToggleUpdateCheck(!app.updateCheckEnabled) + }, + ) + } if (app.skippedReleaseTag != null) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.apps_skip_version_unskip)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.apps_skip_version_unskip), onClick = { showRowOverflow = false onUnskipVersionClick() }, ) } else if (app.isUpdateAvailable && !(app.latestVersion ?: app.latestVersionName).isNullOrBlank()) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.apps_skip_version)) }, + GhsDropdownMenuItem( + text = stringResource(Res.string.apps_skip_version), onClick = { showRowOverflow = false onSkipVersionClick() diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt index 274004093..078018c6b 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt @@ -29,6 +29,7 @@ data class AppsState( val isRefreshing: Boolean = false, val isUpToDateSectionExpanded: Boolean = true, + val isUpdatesSectionExpanded: Boolean = true, val showLinkSheet: Boolean = false, val linkStep: LinkStep = LinkStep.PickApp, @@ -85,6 +86,8 @@ data class AppsState( val showKaoBanner: Boolean = false, val linkSourceHost: String? = null, + + val twoPaneSelectedPackage: String? = null, ) { val filteredDeviceApps: ImmutableList get() { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index 143fd06b9..9a49e3c22 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -328,6 +328,14 @@ class AppsViewModel( _state.update { it.copy(isUpToDateSectionExpanded = !it.isUpToDateSectionExpanded) } } + AppsAction.OnToggleUpdatesSection -> { + _state.update { it.copy(isUpdatesSectionExpanded = !it.isUpdatesSectionExpanded) } + } + + is AppsAction.OnTwoPaneSelect -> { + _state.update { it.copy(twoPaneSelectedPackage = action.packageName) } + } + is AppsAction.OnNavigateToRepo -> { viewModelScope.launch { _events.send( diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt new file mode 100644 index 000000000..ef71f75cf --- /dev/null +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt @@ -0,0 +1,620 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + +package zed.rainxch.apps.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.Update +import androidx.compose.material.icons.outlined.Apps +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.apps.presentation.model.AppItem +import zed.rainxch.apps.presentation.model.UpdateState +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_pending_install +import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_pre_release_on +import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_ready_to_install +import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_updates_ignored +import zed.rainxch.githubstore.core.presentation.res.apps_ignore_updates +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_detail_section_actions +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_detail_section_settings +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_detail_section_status +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_empty_subtitle +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_empty_title +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_advanced_settings +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_installed_label +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_latest_label +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_open_repo +import zed.rainxch.githubstore.core.presentation.res.apps_two_pane_pick_variant +import zed.rainxch.githubstore.core.presentation.res.cancel +import zed.rainxch.githubstore.core.presentation.res.discard_pending_install +import zed.rainxch.githubstore.core.presentation.res.downloading +import zed.rainxch.githubstore.core.presentation.res.error_with_message +import zed.rainxch.githubstore.core.presentation.res.install +import zed.rainxch.githubstore.core.presentation.res.installing +import zed.rainxch.githubstore.core.presentation.res.open +import zed.rainxch.githubstore.core.presentation.res.pre_release_badge +import zed.rainxch.githubstore.core.presentation.res.uninstall +import zed.rainxch.githubstore.core.presentation.res.update +import zed.rainxch.githubstore.core.presentation.res.variant_label_inline + +@Composable +fun AppDetailPane( + appItem: AppItem?, + onOpenApp: () -> Unit, + onUpdateApp: () -> Unit, + onCancelUpdate: () -> Unit, + onUninstall: () -> Unit, + onOpenRepo: () -> Unit, + onTogglePreReleases: (Boolean) -> Unit, + onToggleUpdateCheck: (Boolean) -> Unit, + onOpenAdvancedSettings: () -> Unit, + onPickVariant: () -> Unit, + onInstallPending: () -> Unit, + onDiscardPending: () -> Unit, + modifier: Modifier = Modifier, +) { + if (appItem == null) { + EmptyDetailPane(modifier = modifier) + return + } + + val app = appItem.installedApp + val isBusy = app.isPendingInstall || + appItem.updateState is UpdateState.Downloading || + appItem.updateState is UpdateState.Installing || + appItem.updateState is UpdateState.CheckingUpdate + + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(8.dp)) + DetailHeader(appItem = appItem) + + Spacer(Modifier.height(16.dp)) + SectionLabel(stringResource(Res.string.apps_two_pane_detail_section_status)) + StatusBlock(appItem = appItem) + + Spacer(Modifier.height(20.dp)) + SectionLabel(stringResource(Res.string.apps_two_pane_detail_section_actions)) + PrimaryActionsRow( + appItem = appItem, + isBusy = isBusy, + onOpenApp = onOpenApp, + onUpdateApp = onUpdateApp, + onCancelUpdate = onCancelUpdate, + onInstallPending = onInstallPending, + onDiscardPending = onDiscardPending, + ) + + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + OutlinedButton( + onClick = onOpenRepo, + modifier = Modifier.weight(1f).height(44.dp), + shape = RoundedCornerShape(50), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.apps_two_pane_open_repo)) + } + OutlinedButton( + onClick = onUninstall, + enabled = !isBusy, + modifier = Modifier.height(44.dp), + shape = RoundedCornerShape(50), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.55f)), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Icon( + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.uninstall)) + } + } + + Spacer(Modifier.height(20.dp)) + SectionLabel(stringResource(Res.string.apps_two_pane_detail_section_settings)) + SettingsBlock( + appItem = appItem, + onTogglePreReleases = onTogglePreReleases, + onToggleUpdateCheck = onToggleUpdateCheck, + onOpenAdvancedSettings = onOpenAdvancedSettings, + onPickVariant = onPickVariant, + ) + + Spacer(Modifier.height(24.dp)) + } +} + +@Composable +private fun DetailHeader(appItem: AppItem) { + val app = appItem.installedApp + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(72.dp) + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + InstalledAppIcon( + packageName = app.packageName, + appName = app.appName, + apkFilePath = app.pendingInstallFilePath, + modifier = Modifier.size(56.dp), + ) + } + Spacer(Modifier.width(14.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = app.appName, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = app.packageName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(2.dp)) + Text( + text = "${app.repoOwner}/${app.repoName}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun StatusBlock(appItem: AppItem) { + val app = appItem.installedApp + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(16.dp)) { + StatusRow( + label = stringResource(Res.string.apps_two_pane_installed_label), + value = app.installedVersion, + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + modifier = Modifier.padding(vertical = 10.dp), + ) + StatusRow( + label = stringResource(Res.string.apps_two_pane_latest_label), + value = app.latestVersion ?: "—", + ) + if (app.preferredAssetVariant != null) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + modifier = Modifier.padding(vertical = 10.dp), + ) + StatusRow( + label = stringResource(Res.string.variant_label_inline), + value = app.preferredAssetVariant, + ) + } + + when (val state = appItem.updateState) { + is UpdateState.Downloading -> { + Spacer(Modifier.height(14.dp)) + StatusProgress( + label = stringResource(Res.string.downloading), + progress = null, + ) + } + is UpdateState.Installing -> { + Spacer(Modifier.height(14.dp)) + StatusProgress( + label = stringResource(Res.string.installing), + progress = null, + ) + } + is UpdateState.Error -> { + Spacer(Modifier.height(10.dp)) + Text( + text = stringResource(Res.string.error_with_message, state.message), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + else -> {} + } + + val pills = buildList { + if (app.isPendingInstall) add(stringResource(Res.string.apps_compact_status_pending_install)) + if (app.pendingInstallFilePath != null && !app.isPendingInstall) { + add(stringResource(Res.string.apps_compact_status_ready_to_install)) + } + if (app.includePreReleases) add(stringResource(Res.string.apps_compact_status_pre_release_on)) + if (!app.updateCheckEnabled) add(stringResource(Res.string.apps_compact_status_updates_ignored)) + } + if (pills.isNotEmpty()) { + Spacer(Modifier.height(12.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + pills.forEach { label -> + StatusPill(label) + } + } + } + } + } +} + +@Composable +private fun StatusRow( + label: String, + value: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun StatusProgress( + label: String, + progress: Float?, +) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(6.dp)) + if (progress != null) { + LinearWavyProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth(), + ) + } else { + LinearWavyProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + } +} + +@Composable +private fun StatusPill(label: String) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + ) + } +} + +@Composable +private fun PrimaryActionsRow( + appItem: AppItem, + isBusy: Boolean, + onOpenApp: () -> Unit, + onUpdateApp: () -> Unit, + onCancelUpdate: () -> Unit, + onInstallPending: () -> Unit, + onDiscardPending: () -> Unit, +) { + val app = appItem.installedApp + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + when (appItem.updateState) { + is UpdateState.Downloading, is UpdateState.Installing, is UpdateState.CheckingUpdate -> { + Button( + onClick = onCancelUpdate, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(50), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + ) { + Icon( + imageVector = Icons.Default.Cancel, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.cancel), fontWeight = FontWeight.SemiBold) + } + } + else -> { + if (app.pendingInstallFilePath != null) { + Button( + onClick = onInstallPending, + enabled = !isBusy, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(50), + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.install), fontWeight = FontWeight.SemiBold) + } + OutlinedButton( + onClick = onDiscardPending, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 18.dp), + ) { + Text(stringResource(Res.string.discard_pending_install)) + } + } else if (app.isUpdateAvailable && !app.isPendingInstall) { + Button( + onClick = onUpdateApp, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(50), + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.update), fontWeight = FontWeight.SemiBold) + } + } else { + Button( + onClick = onOpenApp, + enabled = !isBusy, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(50), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Text(stringResource(Res.string.open), fontWeight = FontWeight.SemiBold) + } + } + } + } + } +} + +@Composable +private fun SettingsBlock( + appItem: AppItem, + onTogglePreReleases: (Boolean) -> Unit, + onToggleUpdateCheck: (Boolean) -> Unit, + onOpenAdvancedSettings: () -> Unit, + onPickVariant: () -> Unit, +) { + val app = appItem.installedApp + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), + modifier = Modifier.fillMaxWidth(), + ) { + Column { + SettingsToggleRow( + title = stringResource(Res.string.pre_release_badge), + checked = app.includePreReleases, + onCheckedChange = onTogglePreReleases, + ) + DividerThin() + SettingsToggleRow( + title = stringResource(Res.string.apps_ignore_updates), + checked = !app.updateCheckEnabled, + onCheckedChange = { onToggleUpdateCheck(!it) }, + ) + DividerThin() + SettingsActionRow( + title = stringResource(Res.string.apps_two_pane_pick_variant), + onClick = onPickVariant, + ) + DividerThin() + SettingsActionRow( + title = stringResource(Res.string.apps_two_pane_advanced_settings), + onClick = onOpenAdvancedSettings, + ) + } + } +} + +@Composable +private fun SettingsToggleRow( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + ) + } +} + +@Composable +private fun SettingsActionRow( + title: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainerLow, + onClick = onClick, + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Tune, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(12.dp)) + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp), + ) + } + } +} + +@Composable +private fun DividerThin() { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + ) +} + +@Composable +private fun SectionLabel(text: String) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 8.dp, start = 4.dp), + ) +} + +@Composable +private fun EmptyDetailPane(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(32.dp), + ) { + Icon( + imageVector = Icons.Outlined.Apps, + contentDescription = null, + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(48.dp), + ) + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(Res.string.apps_two_pane_empty_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(Res.string.apps_two_pane_empty_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + } + } +} diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt new file mode 100644 index 000000000..858467ea7 --- /dev/null +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt @@ -0,0 +1,247 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) + +package zed.rainxch.apps.presentation.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearWavyProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.apps.presentation.model.UpdateAllProgress +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_hide +import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_show +import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_subtitle +import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_title_one +import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_title_other +import zed.rainxch.githubstore.core.presentation.res.cancel +import zed.rainxch.githubstore.core.presentation.res.currently_updating +import zed.rainxch.githubstore.core.presentation.res.update_all +import zed.rainxch.githubstore.core.presentation.res.updating_x_of_y + +@Composable +fun UpdatesBanner( + count: Int, + isExpanded: Boolean, + isUpdatingAll: Boolean, + updateAllProgress: UpdateAllProgress?, + updateAllEnabled: Boolean, + onUpdateAll: () -> Unit, + onCancelUpdateAll: () -> Unit, + onToggleExpanded: () -> Unit, + modifier: Modifier = Modifier, +) { + val rotation by animateFloatAsState( + targetValue = if (isExpanded) 0f else -90f, + animationSpec = tween(durationMillis = 180), + label = "updates-banner-chevron", + ) + val title = if (count == 1) { + stringResource(Res.string.apps_updates_banner_title_one) + } else { + stringResource(Res.string.apps_updates_banner_title_other, count) + } + + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.primaryContainer, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)), + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(36.dp) + .padding(2.dp), + contentAlignment = Alignment.Center, + ) { + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.18f), + modifier = Modifier.size(32.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(18.dp), + ) + } + } + } + Spacer(Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = MaterialTheme.colorScheme.onPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringResource(Res.string.apps_updates_banner_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f), + modifier = Modifier + .size(22.dp) + .rotate(rotation), + ) + } + + if (isUpdatingAll && updateAllProgress != null) { + Spacer(Modifier.height(14.dp)) + UpdateAllInlineProgress( + progress = updateAllProgress, + onCancel = onCancelUpdateAll, + ) + } else { + Spacer(Modifier.height(14.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Button( + onClick = onUpdateAll, + enabled = updateAllEnabled, + modifier = Modifier.weight(1f).height(44.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(Res.string.update_all), + fontWeight = FontWeight.SemiBold, + ) + } + OutlinedButton( + onClick = onToggleExpanded, + modifier = Modifier.height(44.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 18.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.35f)), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + Text( + text = if (isExpanded) { + stringResource(Res.string.apps_updates_banner_hide) + } else { + stringResource(Res.string.apps_updates_banner_show) + }, + fontWeight = FontWeight.Medium, + ) + } + } + } + } + } +} + +@Composable +private fun UpdateAllInlineProgress( + progress: UpdateAllProgress, + onCancel: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource( + Res.string.updating_x_of_y, + progress.current, + progress.total, + ), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Text( + text = stringResource( + Res.string.currently_updating, + progress.currentAppName, + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + OutlinedButton( + onClick = onCancel, + modifier = Modifier.height(38.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 14.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.35f)), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + Text(stringResource(Res.string.cancel)) + } + } + Spacer(Modifier.height(10.dp)) + LinearWavyProgressIndicator( + progress = { progress.current.toFloat() / progress.total.coerceAtLeast(1) }, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onPrimaryContainer, + trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.18f), + ) + } +} From 95b9f468cdbde2e17a387b798fd7124a762f8441 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:49 +0500 Subject: [PATCH 045/172] feat(details): hero header, two-layer state, inner About+WhatsNew routes --- .../details/presentation/DetailsRoot.kt | 358 +++++----- .../details/presentation/DetailsState.kt | 59 +- .../details/presentation/DetailsViewModel.kt | 11 +- .../details/presentation/RawDetailsState.kt | 87 +++ .../presentation/RawDetailsStateMapper.kt | 118 ++++ .../details/presentation/about/AboutRoot.kt | 186 ++++++ .../presentation/about/DetailsAboutState.kt | 9 + .../about/DetailsAboutViewModel.kt | 60 ++ .../components/ApkInspectSheet.kt | 9 +- .../presentation/components/AppHeader.kt | 611 ++++++++++-------- .../components/InspectApkButton.kt | 23 +- .../presentation/components/LanguagePicker.kt | 24 +- .../components/LinkedRepoBanner.kt | 3 +- .../components/ReleaseAssetsPicker.kt | 214 +++--- .../components/ReleasesStatusCard.kt | 3 +- .../presentation/components/StatItem.kt | 114 ++-- .../presentation/components/VersionPicker.kt | 275 ++++---- .../components/VersionTypePicker.kt | 22 +- .../presentation/components/sections/About.kt | 119 ++-- .../components/sections/Header.kt | 92 +-- .../presentation/components/sections/Logs.kt | 55 +- .../presentation/components/sections/Owner.kt | 238 +++---- .../components/sections/ReleaseChannel.kt | 18 +- .../components/sections/ReportIssue.kt | 78 ++- .../presentation/components/sections/Stats.kt | 37 +- .../components/sections/WhatsNew.kt | 158 +++-- .../utils/MarkdownImageTransformer.kt | 5 +- .../presentation/utils/MarkdownUtils.kt | 1 - .../whatsnew/DetailsWhatsNewState.kt | 10 + .../whatsnew/DetailsWhatsNewViewModel.kt | 59 ++ .../presentation/whatsnew/WhatsNewRoot.kt | 196 ++++++ 31 files changed, 2050 insertions(+), 1202 deletions(-) create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsState.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsStateMapper.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index 41e411ab4..34764bf9a 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -1,12 +1,17 @@ package zed.rainxch.details.presentation import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.Platform import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -29,8 +34,13 @@ import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.StarBorder import androidx.compose.material3.AlertDialog +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -81,6 +91,7 @@ import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.InstallSource import zed.rainxch.core.domain.model.ContentWidth +import zed.rainxch.core.presentation.components.FloatingPill import zed.rainxch.core.presentation.components.ScrollbarContainer import zed.rainxch.core.presentation.locals.LocalContentWidth import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled @@ -145,6 +156,8 @@ fun DetailsRoot( onNavigateToDeveloperProfile: (username: String) -> Unit, onOpenRepositoryInApp: (repoId: Long) -> Unit, onNavigateToSearchByPlatform: (DiscoveryPlatform) -> Unit, + onNavigateToAbout: (repoId: Long, owner: String, repo: String, sourceHost: String?) -> Unit, + onNavigateToWhatsNew: (repoId: Long, owner: String, repo: String, sourceHost: String?) -> Unit, viewModel: DetailsViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -226,6 +239,26 @@ fun DetailsRoot( state = state, snackbarHostState = snackbarHostState, onAction = onAction, + onReadMoreAbout = state.repository?.let { repo -> + { + onNavigateToAbout( + repo.id, + repo.owner.login, + repo.name, + repo.sourceHost, + ) + } + }, + onReadMoreWhatsNew = state.repository?.let { repo -> + { + onNavigateToWhatsNew( + repo.id, + repo.owner.login, + repo.name, + repo.sourceHost, + ) + } + }, ) state.downgradeWarning?.let { warning -> @@ -233,9 +266,13 @@ fun DetailsRoot( onDismissRequest = { viewModel.onAction(DetailsAction.OnDismissDowngradeWarning) }, + shape = WonkySquircleShape.Dialog, title = { Text( text = stringResource(Res.string.downgrade_requires_uninstall), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), ) }, text = { @@ -280,9 +317,13 @@ fun DetailsRoot( onDismissRequest = { viewModel.onAction(DetailsAction.OnDismissSigningKeyWarning) }, + shape = WonkySquircleShape.Dialog, title = { Text( text = stringResource(Res.string.signing_key_changed_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), ) }, text = { @@ -327,9 +368,13 @@ fun DetailsRoot( onDismissRequest = { viewModel.onAction(DetailsAction.OnDismissUninstallConfirmation) }, + shape = WonkySquircleShape.Dialog, title = { Text( text = stringResource(Res.string.confirm_uninstall_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), ) }, text = { @@ -367,8 +412,14 @@ fun DetailsRoot( onDismissRequest = { viewModel.onAction(DetailsAction.OnDismissUnlinkConfirmation) }, + shape = WonkySquircleShape.Dialog, title = { - Text(text = stringResource(Res.string.details_unlink_external_app_dialog_title)) + Text( + text = stringResource(Res.string.details_unlink_external_app_dialog_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + ) }, text = { Text( @@ -404,8 +455,14 @@ fun DetailsRoot( onDismissRequest = { viewModel.onAction(DetailsAction.DismissExternalInstallerPrompt) }, + shape = WonkySquircleShape.Dialog, title = { - Text(text = stringResource(Res.string.install_permission_unavailable)) + Text( + text = stringResource(Res.string.install_permission_unavailable), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + ) }, text = { Text(text = stringResource(Res.string.install_permission_blocked_message)) @@ -446,6 +503,8 @@ fun DetailsScreen( state: DetailsState, onAction: (DetailsAction) -> Unit, snackbarHostState: SnackbarHostState, + onReadMoreAbout: (() -> Unit)? = null, + onReadMoreWhatsNew: (() -> Unit)? = null, ) { Scaffold( topBar = { @@ -511,7 +570,7 @@ fun DetailsScreen( val density = LocalDensity.current var containerHeightDp by remember { mutableStateOf(0.dp) } - val collapsedSectionHeight = containerHeightDp * 0.7f + val collapsedSectionHeight = containerHeightDp * 0.4f val listState = rememberLazyListState() val isScrollbarEnabled = LocalScrollbarEnabled.current val contentWidthDp = when (LocalContentWidth.current) { @@ -612,6 +671,7 @@ fun DetailsScreen( onToggleTranslation = { onAction(DetailsAction.ToggleWhatsNewTranslation) }, + onReadMore = onReadMoreWhatsNew, ) } @@ -634,6 +694,7 @@ fun DetailsScreen( onToggleTranslation = { onAction(DetailsAction.ToggleAboutTranslation) }, + onReadMore = onReadMoreAbout, ) } } else { @@ -656,6 +717,7 @@ fun DetailsScreen( onToggleTranslation = { onAction(DetailsAction.ToggleAboutTranslation) }, + onReadMore = onReadMoreAbout, ) } @@ -677,6 +739,7 @@ fun DetailsScreen( onToggleTranslation = { onAction(DetailsAction.ToggleWhatsNewTranslation) }, + onReadMore = onReadMoreWhatsNew, ) } } @@ -731,156 +794,62 @@ private fun DetailsTopbar( state: DetailsState, onAction: (DetailsAction) -> Unit, ) { - TopAppBar( - title = { }, - navigationIcon = { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - onAction(DetailsAction.OnNavigateBackClick) - }, - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.navigate_back), - modifier = Modifier.size(24.dp), - ) - } - }, - actions = { + Row( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + FloatingPill( + modifier = Modifier.clickable { + onAction(DetailsAction.OnNavigateBackClick) + }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(20.dp), + ) + } + if (state.repository != null) { Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(50), + ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - if (state.repository != null) { - IconButton( - onClick = { - onAction( - DetailsAction.OnMessage( - messageText = - if (state.isStarred) { - Res.string.unstar_from_github - } else { - Res.string.star_from_github - }, - ), - ) - }, - shapes = IconButtonDefaults.shapes(), - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = - if (state.isStarred) { - Icons.Default.Star - } else { - Icons.Default.StarBorder - }, - contentDescription = - stringResource( - resource = - if (state.isStarred) { - Res.string.repository_starred - } else { - Res.string.repository_not_starred - }, - ), - ) - } - - IconButton( - onClick = { - onAction(DetailsAction.OnToggleFavorite) - }, - shapes = IconButtonDefaults.shapes(), - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = - if (state.isFavourite) { - Icons.Default.Favorite - } else { - Icons.Default.FavoriteBorder - }, - contentDescription = - stringResource( - resource = - if (state.isFavourite) { - Res.string.remove_from_favourites - } else { - Res.string.add_to_favourites - }, - ), - ) - } - - IconButton( - onClick = { - onAction(DetailsAction.OnShareClick) - }, - shapes = IconButtonDefaults.shapes(), - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = stringResource(Res.string.share_repository), - ) - } - } - - state.repository?.htmlUrl?.let { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - onAction(DetailsAction.OpenRepoInBrowser) - }, - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = Icons.Default.OpenInBrowser, - contentDescription = stringResource(Res.string.open_repository), - modifier = Modifier.size(24.dp), - ) - } - } - - if (state.repository != null) { - DetailsOverflowMenu(state = state, onAction = onAction) + Box( + modifier = Modifier + .clickable { onAction(DetailsAction.OnShareClick) } + .padding(horizontal = 12.dp, vertical = 10.dp), + ) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(Res.string.share_repository), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(18.dp), + ) } + Box( + modifier = Modifier + .size(1.dp, 20.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)), + ) + DetailsOverflowMenu(state = state, onAction = onAction) } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - ), - modifier = - Modifier - .shadow( - elevation = 6.dp, - ambientColor = MaterialTheme.colorScheme.surfaceTint, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), - ).background( - Brush.linearGradient( - 0f to MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), - 0.5f to MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), - 1f to MaterialTheme.colorScheme.surface.copy(alpha = 0.85f), - ), - ).background(MaterialTheme.colorScheme.surfaceContainerHighest), - ) + } + } } + @OptIn( ExperimentalTime::class, ExperimentalMaterial3Api::class, @@ -911,39 +880,97 @@ private fun DetailsOverflowMenu( val refreshDisabled = cooldownActive || state.isRefreshing Box { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { menuOpen = true }, - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), + Box( + modifier = Modifier + .clickable { menuOpen = true } + .padding(horizontal = 12.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, ) { Icon( imageVector = Icons.Default.MoreVert, contentDescription = stringResource(Res.string.details_refresh_more_options), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(18.dp), ) } - DropdownMenu( + GhsDropdownMenu( expanded = menuOpen, onDismissRequest = { menuOpen = false }, ) { - DropdownMenuItem( - enabled = !refreshDisabled, - text = { - Text( - text = - if (cooldownActive) { - stringResource(Res.string.details_refresh_cooldown, cooldownSeconds) + GhsDropdownMenuItem( + text = stringResource( + if (state.isStarred) Res.string.repository_starred + else Res.string.repository_not_starred, + ), + leadingIcon = { + Icon( + imageVector = if (state.isStarred) Icons.Default.Star else Icons.Default.StarBorder, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, + onClick = { + menuOpen = false + onAction( + DetailsAction.OnMessage( + messageText = if (state.isStarred) { + Res.string.unstar_from_github } else { - stringResource(Res.string.details_refresh) + Res.string.star_from_github }, + ), + ) + }, + ) + GhsDropdownMenuItem( + text = stringResource( + if (state.isFavourite) Res.string.remove_from_favourites + else Res.string.add_to_favourites, + ), + leadingIcon = { + Icon( + imageVector = if (state.isFavourite) { + Icons.Default.Favorite + } else { + Icons.Default.FavoriteBorder + }, + contentDescription = null, + modifier = Modifier.size(18.dp), ) }, + onClick = { + menuOpen = false + onAction(DetailsAction.OnToggleFavorite) + }, + ) + state.repository?.htmlUrl?.let { + GhsDropdownMenuItem( + text = stringResource(Res.string.open_repository), + leadingIcon = { + Icon( + imageVector = Icons.Default.OpenInBrowser, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, + onClick = { + menuOpen = false + onAction(DetailsAction.OpenRepoInBrowser) + }, + ) + } + GhsDropdownMenuItem( + enabled = !refreshDisabled, + text = if (cooldownActive) { + stringResource(Res.string.details_refresh_cooldown, cooldownSeconds) + } else { + stringResource(Res.string.details_refresh) + }, leadingIcon = { Icon( imageVector = Icons.Default.Refresh, contentDescription = null, + modifier = Modifier.size(18.dp), ) }, onClick = { @@ -952,14 +979,13 @@ private fun DetailsOverflowMenu( }, ) if (state.installedApp?.installSource == InstallSource.MANUAL) { - DropdownMenuItem( - text = { - Text(text = stringResource(Res.string.details_unlink_external_app_menu)) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.details_unlink_external_app_menu), leadingIcon = { Icon( imageVector = Icons.Default.LinkOff, contentDescription = null, + modifier = Modifier.size(18.dp), ) }, onClick = { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt index 2db05e3ff..c960522c1 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt @@ -7,8 +7,6 @@ import zed.rainxch.core.domain.model.GithubRepoSummary import zed.rainxch.core.domain.model.GithubUserProfile import zed.rainxch.core.domain.model.InstalledApp import zed.rainxch.core.domain.model.SystemArchitecture -import zed.rainxch.core.domain.model.isEffectivelyPreRelease -import zed.rainxch.core.domain.util.VersionMath import zed.rainxch.details.domain.model.ReleaseCategory import zed.rainxch.details.domain.model.RepoStats import zed.rainxch.details.presentation.model.AttestationStatus @@ -27,10 +25,8 @@ data class DetailsState( val errorMessage: String? = null, val userProfile: GithubUserProfile? = null, val repository: GithubRepoSummary? = null, - val primaryAsset: GithubAsset? = null, val installableAssets: List = emptyList(), - val selectedRelease: GithubRelease? = null, val allReleases: List = emptyList(), val releasesLoadFailed: Boolean = false, @@ -57,14 +53,12 @@ data class DetailsState( val isAppManagerAvailable: Boolean = false, val isAppManagerEnabled: Boolean = false, val installedApp: InstalledApp? = null, - val installedApps: List = emptyList(), val isFavourite: Boolean = false, val isStarred: Boolean = false, val isTrackingApp: Boolean = false, val isAboutExpanded: Boolean = false, val isWhatsNewExpanded: Boolean = false, - val aboutMeasuredHeightPx: Float? = null, val whatsNewMeasuredHeightPx: Float? = null, val aboutTranslation: TranslationState = TranslationState(), @@ -80,62 +74,19 @@ data class DetailsState( val showUninstallConfirmation: Boolean = false, val showUnlinkConfirmation: Boolean = false, val attestationStatus: AttestationStatus = AttestationStatus.UNCHECKED, - val stalledStableSinceDays: Int? = null, - val mergedChangelog: String? = null, - val mergedChangelogBaseTag: String? = null, - val latestStableHasInstallableAsset: Boolean = false, - val apkInspection: ApkInspection? = null, - val isApkInspectSheetVisible: Boolean = false, - val isApkInspectLoading: Boolean = false, - val isApkInspectCoachmarkPending: Boolean = false, - val isChannelChipCoachmarkPending: Boolean = false, - val showAllPlatforms: Boolean = false, -) { - val filteredReleases: List - get() = - when (selectedReleaseCategory) { - ReleaseCategory.STABLE -> allReleases.filter { !it.isEffectivelyPreRelease() } - ReleaseCategory.PRE_RELEASE -> allReleases.filter { it.isEffectivelyPreRelease() } - ReleaseCategory.ALL -> allReleases - } - - val latestStableRelease: GithubRelease? - get() = - allReleases - .filter { !it.isEffectivelyPreRelease() } - .maxByOrNull { it.publishedAt } - - val canSwitchToStable: Boolean - get() { - val app = installedApp ?: return false - val stable = latestStableRelease ?: return false - if (!latestStableHasInstallableAsset) return false - val installedIsPreRelease = - allReleases.firstOrNull { VersionMath.isSameVersion(it.tagName, app.installedVersion) } - ?.isEffectivelyPreRelease() == true - if (!installedIsPreRelease) return false - - return !VersionMath.isSameVersion(stable.tagName, app.installedVersion) - } - val isPendingInstallReady: Boolean - get() { - val app = installedApp ?: return false - val parkedVersion = app.pendingInstallVersion ?: return false - val parkedAsset = app.pendingInstallAssetName ?: return false - if (app.pendingInstallFilePath.isNullOrBlank()) return false - val tag = selectedRelease?.tagName ?: return false - val assetName = primaryAsset?.name ?: return false - return parkedVersion == tag && parkedAsset == assetName - } -} + val filteredReleases: List = emptyList(), + val latestStableRelease: GithubRelease? = null, + val canSwitchToStable: Boolean = false, + val isPendingInstallReady: Boolean = false, +) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 95b04536f..f4ea3080b 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -138,8 +139,8 @@ class DetailsViewModel( private var aboutTranslationJob: Job? = null private var whatsNewTranslationJob: Job? = null - private val _state = MutableStateFlow(DetailsState()) - val state = + private val _state = MutableStateFlow(RawDetailsState()) + val state: StateFlow = _state .onStart { if (!hasLoadedInitialData) { @@ -151,7 +152,9 @@ class DetailsViewModel( hasLoadedInitialData = true } - }.stateIn( + } + .map { it.toView() } + .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), DetailsState(), @@ -687,7 +690,7 @@ class DetailsViewModel( } private fun switchToStable() { - val stable = _state.value.latestStableRelease ?: return + val stable = _state.value.latestStableRelease() ?: return val (_, primary) = recomputeAssetsForRelease(stable, _state.value.installedApp) if (primary == null) { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsState.kt new file mode 100644 index 000000000..727ebbdfb --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsState.kt @@ -0,0 +1,87 @@ +package zed.rainxch.details.presentation + +import zed.rainxch.core.domain.model.ApkInspection +import zed.rainxch.core.domain.model.GithubAsset +import zed.rainxch.core.domain.model.GithubRelease +import zed.rainxch.core.domain.model.GithubRepoSummary +import zed.rainxch.core.domain.model.GithubUserProfile +import zed.rainxch.core.domain.model.InstalledApp +import zed.rainxch.core.domain.model.SystemArchitecture +import zed.rainxch.details.domain.model.ReleaseCategory +import zed.rainxch.details.domain.model.RepoStats +import zed.rainxch.details.presentation.model.AttestationStatus +import zed.rainxch.details.presentation.model.DowngradeWarning +import zed.rainxch.details.presentation.model.DownloadStage +import zed.rainxch.details.presentation.model.InstallLogItem +import zed.rainxch.details.presentation.model.SigningKeyWarning +import zed.rainxch.details.presentation.model.TranslationState +import zed.rainxch.details.presentation.model.TranslationTarget + +internal data class RawDetailsState( + val isLoading: Boolean = true, + val isCurrentUserOwner: Boolean = false, + val isRefreshing: Boolean = false, + val refreshCooldownUntilEpochMs: Long? = null, + val errorMessage: String? = null, + val userProfile: GithubUserProfile? = null, + val repository: GithubRepoSummary? = null, + val primaryAsset: GithubAsset? = null, + val installableAssets: List = emptyList(), + val selectedRelease: GithubRelease? = null, + val allReleases: List = emptyList(), + val releasesLoadFailed: Boolean = false, + val isRetryingReleases: Boolean = false, + val isReleaseSelectorVisible: Boolean = false, + val selectedReleaseCategory: ReleaseCategory = ReleaseCategory.STABLE, + val isVersionPickerVisible: Boolean = false, + val stats: RepoStats? = null, + val readmeMarkdown: String? = null, + val readmeLanguage: String? = null, + val installLogs: List = emptyList(), + val isDownloading: Boolean = false, + val downloadProgressPercent: Int? = null, + val downloadedBytes: Long = 0L, + val totalBytes: Long? = null, + val isInstalling: Boolean = false, + val downloadError: String? = null, + val installError: String? = null, + val downloadStage: DownloadStage = DownloadStage.IDLE, + val systemArchitecture: SystemArchitecture = SystemArchitecture.UNKNOWN, + val isObtainiumAvailable: Boolean = false, + val isObtainiumEnabled: Boolean = false, + val isInstallDropdownExpanded: Boolean = false, + val isAppManagerAvailable: Boolean = false, + val isAppManagerEnabled: Boolean = false, + val installedApp: InstalledApp? = null, + val installedApps: List = emptyList(), + val isFavourite: Boolean = false, + val isStarred: Boolean = false, + val isTrackingApp: Boolean = false, + val isAboutExpanded: Boolean = false, + val isWhatsNewExpanded: Boolean = false, + val aboutMeasuredHeightPx: Float? = null, + val whatsNewMeasuredHeightPx: Float? = null, + val aboutTranslation: TranslationState = TranslationState(), + val whatsNewTranslation: TranslationState = TranslationState(), + val isLanguagePickerVisible: Boolean = false, + val languagePickerTarget: TranslationTarget? = null, + val deviceLanguageCode: String = "en", + val isComingFromUpdate: Boolean = false, + val downgradeWarning: DowngradeWarning? = null, + val signingKeyWarning: SigningKeyWarning? = null, + val showExternalInstallerPrompt: Boolean = false, + val pendingInstallFilePath: String? = null, + val showUninstallConfirmation: Boolean = false, + val showUnlinkConfirmation: Boolean = false, + val attestationStatus: AttestationStatus = AttestationStatus.UNCHECKED, + val stalledStableSinceDays: Int? = null, + val mergedChangelog: String? = null, + val mergedChangelogBaseTag: String? = null, + val latestStableHasInstallableAsset: Boolean = false, + val apkInspection: ApkInspection? = null, + val isApkInspectSheetVisible: Boolean = false, + val isApkInspectLoading: Boolean = false, + val isApkInspectCoachmarkPending: Boolean = false, + val isChannelChipCoachmarkPending: Boolean = false, + val showAllPlatforms: Boolean = false, +) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsStateMapper.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsStateMapper.kt new file mode 100644 index 000000000..53fcf778d --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/RawDetailsStateMapper.kt @@ -0,0 +1,118 @@ +package zed.rainxch.details.presentation + +import zed.rainxch.core.domain.model.GithubRelease +import zed.rainxch.core.domain.model.isEffectivelyPreRelease +import zed.rainxch.core.domain.util.VersionMath +import zed.rainxch.details.domain.model.ReleaseCategory + +internal fun RawDetailsState.toView(): DetailsState { + val filteredReleases = when (selectedReleaseCategory) { + ReleaseCategory.STABLE -> allReleases.filter { !it.isEffectivelyPreRelease() } + ReleaseCategory.PRE_RELEASE -> allReleases.filter { it.isEffectivelyPreRelease() } + ReleaseCategory.ALL -> allReleases + } + val latestStableRelease = allReleases + .filter { !it.isEffectivelyPreRelease() } + .maxByOrNull { it.publishedAt } + val canSwitchToStable = computeCanSwitchToStable(latestStableRelease) + val isPendingInstallReady = computeIsPendingInstallReady() + + return DetailsState( + isLoading = isLoading, + isCurrentUserOwner = isCurrentUserOwner, + isRefreshing = isRefreshing, + refreshCooldownUntilEpochMs = refreshCooldownUntilEpochMs, + errorMessage = errorMessage, + userProfile = userProfile, + repository = repository, + primaryAsset = primaryAsset, + installableAssets = installableAssets, + selectedRelease = selectedRelease, + allReleases = allReleases, + releasesLoadFailed = releasesLoadFailed, + isRetryingReleases = isRetryingReleases, + isReleaseSelectorVisible = isReleaseSelectorVisible, + selectedReleaseCategory = selectedReleaseCategory, + isVersionPickerVisible = isVersionPickerVisible, + stats = stats, + readmeMarkdown = readmeMarkdown, + readmeLanguage = readmeLanguage, + installLogs = installLogs, + isDownloading = isDownloading, + downloadProgressPercent = downloadProgressPercent, + downloadedBytes = downloadedBytes, + totalBytes = totalBytes, + isInstalling = isInstalling, + downloadError = downloadError, + installError = installError, + downloadStage = downloadStage, + systemArchitecture = systemArchitecture, + isObtainiumAvailable = isObtainiumAvailable, + isObtainiumEnabled = isObtainiumEnabled, + isInstallDropdownExpanded = isInstallDropdownExpanded, + isAppManagerAvailable = isAppManagerAvailable, + isAppManagerEnabled = isAppManagerEnabled, + installedApp = installedApp, + installedApps = installedApps, + isFavourite = isFavourite, + isStarred = isStarred, + isTrackingApp = isTrackingApp, + isAboutExpanded = isAboutExpanded, + isWhatsNewExpanded = isWhatsNewExpanded, + aboutMeasuredHeightPx = aboutMeasuredHeightPx, + whatsNewMeasuredHeightPx = whatsNewMeasuredHeightPx, + aboutTranslation = aboutTranslation, + whatsNewTranslation = whatsNewTranslation, + isLanguagePickerVisible = isLanguagePickerVisible, + languagePickerTarget = languagePickerTarget, + deviceLanguageCode = deviceLanguageCode, + isComingFromUpdate = isComingFromUpdate, + downgradeWarning = downgradeWarning, + signingKeyWarning = signingKeyWarning, + showExternalInstallerPrompt = showExternalInstallerPrompt, + pendingInstallFilePath = pendingInstallFilePath, + showUninstallConfirmation = showUninstallConfirmation, + showUnlinkConfirmation = showUnlinkConfirmation, + attestationStatus = attestationStatus, + stalledStableSinceDays = stalledStableSinceDays, + mergedChangelog = mergedChangelog, + mergedChangelogBaseTag = mergedChangelogBaseTag, + latestStableHasInstallableAsset = latestStableHasInstallableAsset, + apkInspection = apkInspection, + isApkInspectSheetVisible = isApkInspectSheetVisible, + isApkInspectLoading = isApkInspectLoading, + isApkInspectCoachmarkPending = isApkInspectCoachmarkPending, + isChannelChipCoachmarkPending = isChannelChipCoachmarkPending, + showAllPlatforms = showAllPlatforms, + filteredReleases = filteredReleases, + latestStableRelease = latestStableRelease, + canSwitchToStable = canSwitchToStable, + isPendingInstallReady = isPendingInstallReady, + ) +} + +internal fun RawDetailsState.latestStableRelease(): GithubRelease? = + allReleases + .filter { !it.isEffectivelyPreRelease() } + .maxByOrNull { it.publishedAt } + +private fun RawDetailsState.computeCanSwitchToStable(latestStable: GithubRelease?): Boolean { + val app = installedApp ?: return false + val stable = latestStable ?: return false + if (!latestStableHasInstallableAsset) return false + val installedIsPreRelease = allReleases + .firstOrNull { VersionMath.isSameVersion(it.tagName, app.installedVersion) } + ?.isEffectivelyPreRelease() == true + if (!installedIsPreRelease) return false + return !VersionMath.isSameVersion(stable.tagName, app.installedVersion) +} + +private fun RawDetailsState.computeIsPendingInstallReady(): Boolean { + val app = installedApp ?: return false + val parkedVersion = app.pendingInstallVersion ?: return false + val parkedAsset = app.pendingInstallAssetName ?: return false + if (app.pendingInstallFilePath.isNullOrBlank()) return false + val tag = selectedRelease?.tagName ?: return false + val assetName = primaryAsset?.name ?: return false + return parkedVersion == tag && parkedAsset == assetName +} diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt new file mode 100644 index 000000000..fce75716f --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt @@ -0,0 +1,186 @@ +package zed.rainxch.details.presentation.about + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.compose.elements.MarkdownText +import io.ktor.client.HttpClient +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named +import zed.rainxch.core.presentation.components.buttons.IconButton +import zed.rainxch.core.presentation.vocabulary.Squiggle +import zed.rainxch.details.presentation.markdown.githubStoreMarkdownComponents +import zed.rainxch.details.presentation.utils.MarkdownImageTransformer +import zed.rainxch.details.presentation.utils.rememberMarkdownColors +import zed.rainxch.details.presentation.utils.rememberMarkdownTypography +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.cd_back +import zed.rainxch.githubstore.core.presentation.res.details_about_screen_title + +@Composable +fun AboutRoot( + repositoryId: Long, + owner: String, + repo: String, + sourceHost: String?, + onNavigateBack: () -> Unit, + viewModel: DetailsAboutViewModel = koinViewModel { + parametersOf(repositoryId, owner, repo, sourceHost) + }, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + AboutScreen( + state = state, + onBack = onNavigateBack, + onRetry = viewModel::retry, + ) +} + +@Composable +private fun AboutScreen( + state: DetailsAboutState, + onBack: () -> Unit, + onRetry: () -> Unit, +) { + val isDark = androidx.compose.foundation.isSystemInDarkTheme() + val probeClient = koinInject(qualifier = named("test")) + val imageTransformer = remember(probeClient) { MarkdownImageTransformer(probeClient) } + val colors = rememberMarkdownColors() + val typography = rememberMarkdownTypography() + val components = remember(isDark, imageTransformer) { + githubStoreMarkdownComponents(imageTransformer, isDark) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .systemBarsPadding(), + ) { + AboutTopBar(title = state.repoName, onBack = onBack) + when { + state.isLoading -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { CircularProgressIndicator() } + + state.errorMessage != null -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = state.errorMessage, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + Spacer(Modifier.size(8.dp)) + Text( + text = "Retry", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(8.dp) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) + } + } + + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + item(key = "header") { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(Res.string.details_about_screen_title), + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 26.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + state.readmeLanguage?.let { lang -> + Spacer(Modifier.height(4.dp)) + Text( + text = lang, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + item(key = "markdown") { + Markdown( + content = state.readmeMarkdown, + colors = colors, + typography = typography, + imageTransformer = imageTransformer, + components = components, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} + +@Composable +private fun AboutTopBar(title: String, onBack: () -> Unit) { + androidx.compose.foundation.layout.Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.cd_back), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp), + ) + } +} diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt new file mode 100644 index 000000000..c8448e41d --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt @@ -0,0 +1,9 @@ +package zed.rainxch.details.presentation.about + +data class DetailsAboutState( + val isLoading: Boolean = true, + val repoName: String = "", + val readmeMarkdown: String = "", + val readmeLanguage: String? = null, + val errorMessage: String? = null, +) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt new file mode 100644 index 000000000..964b895f8 --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt @@ -0,0 +1,60 @@ +package zed.rainxch.details.presentation.about + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import zed.rainxch.details.domain.repository.DetailsRepository + +class DetailsAboutViewModel( + private val repositoryId: Long, + private val owner: String, + private val repo: String, + private val sourceHost: String?, + private val detailsRepository: DetailsRepository, +) : ViewModel() { + + private val _state = MutableStateFlow(DetailsAboutState()) + val state = _state.asStateFlow() + + init { + load() + } + + fun retry() { + load() + } + + private fun load() { + viewModelScope.launch { + _state.update { it.copy(isLoading = true, errorMessage = null) } + runCatching { + val resolved = if (owner.isNotBlank() && repo.isNotBlank()) { + detailsRepository.getRepositoryByOwnerAndName(owner, repo, sourceHost) + } else { + detailsRepository.getRepositoryById(repositoryId) + } + val readme = detailsRepository.getReadme( + owner = resolved.owner.login, + repo = resolved.name, + defaultBranch = resolved.defaultBranch, + sourceHost = sourceHost, + ) + resolved to readme + }.onSuccess { (resolved, readme) -> + _state.update { + it.copy( + isLoading = false, + repoName = resolved.name, + readmeMarkdown = readme?.first.orEmpty(), + readmeLanguage = readme?.second, + ) + } + }.onFailure { e -> + _state.update { it.copy(isLoading = false, errorMessage = e.message ?: "Failed to load") } + } + } + } +} diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt index b7e6860cf..3f5b88ca5 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt @@ -28,7 +28,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -89,12 +89,9 @@ fun ApkInspectSheet( isLoading: Boolean, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = onDismiss, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { when { isLoading -> LoadingState() diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt index 67cf82d60..2480fea19 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt @@ -1,8 +1,13 @@ package zed.rainxch.details.presentation.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,54 +18,83 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material.icons.filled.Update +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.outlined.AccountTree +import androidx.compose.material.icons.outlined.Star import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.skydoves.landscapist.coil3.CoilImage +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.GithubRelease import zed.rainxch.core.domain.model.GithubRepoSummary import zed.rainxch.core.domain.model.GithubUserProfile import zed.rainxch.core.domain.model.InstalledApp -import zed.rainxch.core.domain.util.VersionMath +import zed.rainxch.core.presentation.color.avatarColorFor import zed.rainxch.core.presentation.components.ForkBadge +import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.components.OfficialBadge import zed.rainxch.core.presentation.components.PlatformChip -import zed.rainxch.core.presentation.utils.formatReleasedAt +import zed.rainxch.core.presentation.theme.shapes.CornerRadii +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.utils.formatCount +import zed.rainxch.details.domain.model.RepoStats import zed.rainxch.details.presentation.model.DownloadStage import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.by_author import zed.rainxch.githubstore.core.presentation.res.installed -import zed.rainxch.githubstore.core.presentation.res.installed_version import zed.rainxch.githubstore.core.presentation.res.no_description import zed.rainxch.githubstore.core.presentation.res.pending_install import zed.rainxch.githubstore.core.presentation.res.update_available +private val HeaderShape = WonkySquircleShape( + topStart = CornerRadii(30.dp, 24.dp), + topEnd = CornerRadii(24.dp, 30.dp), + bottomEnd = CornerRadii(28.dp, 22.dp), + bottomStart = CornerRadii(22.dp, 28.dp), +) + +private fun Color.normalizedForStripe(isDark: Boolean): Color { + val r = (red * 255f).toInt().coerceIn(0, 255) + val g = (green * 255f).toInt().coerceIn(0, 255) + val b = (blue * 255f).toInt().coerceIn(0, 255) + val lum = (r + g + b) / 3 + val target = if (isDark) 170 else 140 + val minLum = if (isDark) 90 else 70 + if (lum >= minLum) return this + val factor = target.toFloat() / lum.coerceAtLeast(1).toFloat() + return Color( + red = (r * factor).toInt().coerceIn(0, 255), + green = (g * factor).toInt().coerceIn(0, 255), + blue = (b * factor).toInt().coerceIn(0, 255), + alpha = 255, + ) +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class) @Composable fun AppHeader( @@ -68,322 +102,339 @@ fun AppHeader( repository: GithubRepoSummary, release: GithubRelease?, installedApp: InstalledApp?, + stats: RepoStats?, modifier: Modifier = Modifier, downloadStage: DownloadStage = DownloadStage.IDLE, downloadProgress: Int? = null, isCurrentUserOwner: Boolean = false, onPlatformClick: ((DiscoveryPlatform) -> Unit)? = null, + onOwnerClick: () -> Unit = {}, ) { + val isDark = isSystemInDarkTheme() + val surface = MaterialTheme.colorScheme.surface + val avatarUrl = author?.avatarUrl ?: repository.owner.avatarUrl + val rawAccent = avatarColorFor(avatarUrl, MaterialTheme.colorScheme.primary) + val normalizedAccent = remember(rawAccent, isDark) { rawAccent.normalizedForStripe(isDark) } + val tintFraction = if (isDark) 0.10f else 0.06f + val animatedAccent by animateColorAsState( + targetValue = normalizedAccent, + animationSpec = tween(durationMillis = 1800, easing = LinearOutSlowInEasing), + label = "details-hero-accent", + ) + val animatedSurface by animateColorAsState( + targetValue = lerp(surface, normalizedAccent, tintFraction), + animationSpec = tween(durationMillis = 1800, easing = LinearOutSlowInEasing), + label = "details-hero-surface", + ) + val stripeBase = if (isDark) animatedAccent.copy(alpha = 0.18f) else animatedAccent.copy(alpha = 0.12f) + val stripeLineThick = if (isDark) animatedAccent.copy(alpha = 0.45f) else animatedAccent.copy(alpha = 0.55f) + val stripeLineThin = if (isDark) animatedAccent.copy(alpha = 0.22f) else animatedAccent.copy(alpha = 0.30f) + val avatarBg = if (isDark) animatedAccent.copy(alpha = 0.20f) else animatedAccent.copy(alpha = 0.14f) + val borderColor = MaterialTheme.colorScheme.outline + val animatedProgress by animateFloatAsState( targetValue = (downloadProgress ?: 0) / 100f, animationSpec = tween(durationMillis = 500), - label = "avatar_progress_animation", + label = "avatar-progress", ) - val supportedPlatforms by remember(release?.assets) { - derivedStateOf { - derivePlatformsFromAssets(release) + val supportedPlatforms = remember(release?.assets) { + val names = release?.assets?.map { it.name.lowercase() }.orEmpty() + buildList { + if (names.any { it.endsWith(".apk") }) add(DiscoveryPlatform.Android) + if (names.any { it.endsWith(".exe") || it.endsWith(".msi") }) add(DiscoveryPlatform.Windows) + if (names.any { it.endsWith(".dmg") || it.endsWith(".pkg") }) add(DiscoveryPlatform.Macos) + if (names.any { + it.endsWith(".appimage") || + it.endsWith(".deb") || + it.endsWith(".rpm") || + it.endsWith(".pkg.tar.zst") + } + ) add(DiscoveryPlatform.Linux) } } - Column( - modifier = modifier.fillMaxWidth(), + Box( + modifier = modifier + .fillMaxWidth() + .clip(HeaderShape) + .background(animatedSurface) + .border(1.5.dp, borderColor, HeaderShape), ) { - Row( - verticalAlignment = Alignment.Top, - ) { + Column(modifier = Modifier.fillMaxWidth()) { Box( - contentAlignment = Alignment.Center, - modifier = Modifier.size(100.dp), - ) { - CoilImage( - - imageModel = { author?.avatarUrl ?: repository.owner.avatarUrl }, - modifier = - Modifier - .size(100.dp) - .clip(CircleShape) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant, - shape = CircleShape, - ), - loading = { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularWavyProgressIndicator() + modifier = Modifier + .fillMaxWidth() + .height(76.dp) + .clipToBounds() + .drawBehind { + drawRect(color = stripeBase) + val thick = 9.dp.toPx() + val thin = 2.5.dp.toPx() + val gapAfterThick = 10.dp.toPx() + val gapBetweenThin = 6.dp.toPx() + val cycle = thick + gapAfterThick + thin + gapBetweenThin + thin + gapAfterThick + var x = -size.height + while (x < size.width + size.height) { + drawLine( + color = stripeLineThick, + start = Offset(x, size.height), + end = Offset(x + size.height, 0f), + strokeWidth = thick, + cap = StrokeCap.Round, + ) + var xt = x + thick + gapAfterThick + drawLine( + color = stripeLineThin, + start = Offset(xt, size.height), + end = Offset(xt + size.height, 0f), + strokeWidth = thin, + cap = StrokeCap.Round, + ) + xt += thin + gapBetweenThin + drawLine( + color = stripeLineThin, + start = Offset(xt, size.height), + end = Offset(xt + size.height, 0f), + strokeWidth = thin, + cap = StrokeCap.Round, + ) + x += cycle } }, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 130.dp, end = 20.dp, top = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = author?.login ?: repository.owner.login, + style = MaterialTheme.typography.labelMedium.copy( + fontSize = 13.sp, + ), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f, fill = false) + .clickable(onClick = onOwnerClick), ) - - if (downloadStage != DownloadStage.IDLE) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.size(100.dp), - ) { - when (downloadStage) { - DownloadStage.DOWNLOADING -> { - CircularProgressIndicator( - progress = { 1f }, - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), - strokeWidth = 4.dp, - ) - - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.primary, - strokeWidth = 4.dp, - strokeCap = StrokeCap.Round, - ) - } - - DownloadStage.VERIFYING, DownloadStage.INSTALLING -> { - CircularProgressIndicator( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.primary, - strokeWidth = 4.dp, - strokeCap = StrokeCap.Round, - ) - } - } - } - } + if (isCurrentUserOwner) OfficialBadge() } - - Spacer(Modifier.width(16.dp)) - - Column( - modifier = Modifier.weight(1f), + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 130.dp, end = 20.dp, top = 2.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = repository.name, + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.Black, + fontSize = 30.sp, + letterSpacing = (-0.4).sp, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + if (repository.isFork) ForkBadge() + } + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), ) { + val statColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f) + val statStyle = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + ) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { + Icon( + imageVector = Icons.Outlined.Star, + contentDescription = null, + tint = statColor, + modifier = Modifier.size(15.dp), + ) Text( - text = repository.name, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), + text = formatCount(stats?.stars ?: repository.stargazersCount), + style = statStyle, + color = statColor, ) - - if (repository.isFork) { - ForkBadge() - } } - author?.login?.let { author -> + val forksValue = stats?.forks ?: repository.forksCount + if (forksValue > 0) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( - text = stringResource(Res.string.by_author, author), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), + Icon( + imageVector = Icons.Outlined.AccountTree, + contentDescription = null, + tint = statColor, + modifier = Modifier.size(15.dp), ) - - if (isCurrentUserOwner) { - OfficialBadge() - } - } - } - - Spacer(Modifier.height(8.dp)) - - if (installedApp != null) { - when { - installedApp.isPendingInstall -> { - PendingInstallBadge() - } - - else -> { - InstallStatusBadge( - isUpdateAvailable = installedApp.isUpdateAvailable, - ) - } - } - } - - Spacer(Modifier.height(8.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - release?.tagName?.let { Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.secondary, + text = formatCount(forksValue), + style = statStyle, + color = statColor, ) } - - if (installedApp != null && - !VersionMath.isExactSameVersion(installedApp.installedVersion, release?.tagName) + } + val downloadsValue = stats?.totalDownloads ?: repository.downloadCount + if (downloadsValue > 0) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + tint = statColor, + modifier = Modifier.size(15.dp), + ) Text( - text = - stringResource( - Res.string.installed_version, - installedApp.installedVersion, - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = formatCount(downloadsValue), + style = statStyle, + color = statColor, ) } } - - release?.publishedAt?.let { publishedAt -> - Spacer(Modifier.height(4.dp)) - + val licenseValue = stats?.license + if (!licenseValue.isNullOrBlank()) { Text( - text = formatReleasedAt(publishedAt), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, + text = licenseValue, + style = statStyle, + color = statColor, ) } } - } - - if (supportedPlatforms.isNotEmpty()) { Spacer(Modifier.height(12.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - supportedPlatforms.forEach { platform -> - PlatformChip( - platform = platform, - onClick = onPlatformClick?.let { handler -> { handler(platform) } }, - ) + Text( + text = repository.description ?: stringResource(Res.string.no_description), + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 14.sp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 20.dp), + ) + if (installedApp != null) { + Spacer(Modifier.height(12.dp)) + Box(modifier = Modifier.padding(horizontal = 20.dp)) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .border( + width = 1.dp, + color = when { + installedApp.isPendingInstall -> + MaterialTheme.colorScheme.secondary + installedApp.isUpdateAvailable -> + MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.primary + }, + shape = RoundedCornerShape(50), + ) + .padding(horizontal = 12.dp, vertical = 5.dp), + ) { + Text( + text = stringResource( + when { + installedApp.isPendingInstall -> Res.string.pending_install + installedApp.isUpdateAvailable -> Res.string.update_available + else -> Res.string.installed + }, + ), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), + color = when { + installedApp.isPendingInstall -> + MaterialTheme.colorScheme.secondary + installedApp.isUpdateAvailable -> + MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.primary + }, + ) + } } } - } - - Spacer(Modifier.height(16.dp)) - - Text( - text = repository.description ?: stringResource(Res.string.no_description), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} - -private fun derivePlatformsFromAssets(release: GithubRelease?): List { - if (release == null) return emptyList() - val names = release.assets.map { it.name.lowercase() } - return buildList { - if (names.any { it.endsWith(".apk") }) add(DiscoveryPlatform.Android) - if (names.any { it.endsWith(".exe") || it.endsWith(".msi") }) add(DiscoveryPlatform.Windows) - if (names.any { it.endsWith(".dmg") || it.endsWith(".pkg") }) add(DiscoveryPlatform.Macos) - if (names.any { - it.endsWith(".appimage") || - it.endsWith(".deb") || - it.endsWith(".rpm") || - it.endsWith(".pkg.tar.zst") + if (supportedPlatforms.isNotEmpty()) { + Spacer(Modifier.height(14.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(horizontal = 20.dp), + ) { + supportedPlatforms.forEach { platform -> + PlatformChip( + platform = platform, + onClick = onPlatformClick?.let { handler -> { handler(platform) } }, + ) + } + } } - ) { - add( - DiscoveryPlatform.Linux, - ) - } - } -} - -@Composable -fun InstallStatusBadge( - isUpdateAvailable: Boolean, - modifier: Modifier = Modifier, -) { - val backgroundColor = - if (isUpdateAvailable) { - MaterialTheme.colorScheme.tertiaryContainer - } else { - MaterialTheme.colorScheme.primaryContainer - } - - val textColor = - if (isUpdateAvailable) { - MaterialTheme.colorScheme.onTertiaryContainer - } else { - MaterialTheme.colorScheme.onPrimaryContainer + Spacer(Modifier.height(20.dp)) } - - val icon = - if (isUpdateAvailable) { - Icons.Default.Update - } else { - Icons.Default.CheckCircle - } - - val text = - if (isUpdateAvailable) { - stringResource(Res.string.update_available) - } else { - stringResource(Res.string.installed) - } - - Surface( - modifier = modifier, - shape = RoundedCornerShape(12.dp), - color = backgroundColor, - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = 20.dp, y = 30.dp) + .size(100.dp), + contentAlignment = Alignment.Center, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = textColor, - ) - Text( - text = text, - style = MaterialTheme.typography.labelSmall, - color = textColor, - fontWeight = FontWeight.SemiBold, - ) - } - } -} - -@Composable -fun PendingInstallBadge(modifier: Modifier = Modifier) { - Surface( - modifier = modifier, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - imageVector = Icons.Default.Schedule, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer, - ) - Text( - text = stringResource(Res.string.pending_install), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - fontWeight = FontWeight.SemiBold, - ) + Box( + modifier = Modifier + .size(100.dp) + .clip(CircleShape) + .background(avatarBg) + .border(2.5.dp, animatedAccent, CircleShape), + contentAlignment = Alignment.Center, + ) { + GitHubStoreImage( + imageModel = { avatarUrl }, + modifier = Modifier.size(92.dp).clip(CircleShape), + extractDominantFor = avatarUrl, + ) + } + if (downloadStage != DownloadStage.IDLE) { + when (downloadStage) { + DownloadStage.DOWNLOADING -> { + CircularProgressIndicator( + progress = { 1f }, + modifier = Modifier.fillMaxSize(), + color = animatedAccent.copy(alpha = 0.2f), + strokeWidth = 4.dp, + ) + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier.fillMaxSize(), + color = animatedAccent, + strokeWidth = 4.dp, + strokeCap = StrokeCap.Round, + ) + } + DownloadStage.VERIFYING, DownloadStage.INSTALLING -> { + CircularProgressIndicator( + modifier = Modifier.fillMaxSize(), + color = animatedAccent, + strokeWidth = 4.dp, + strokeCap = StrokeCap.Round, + ) + } + } + } } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt index 5c774562a..5f793ac64 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Icon @@ -37,8 +38,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import org.jetbrains.compose.resources.stringResource @@ -124,14 +127,14 @@ private fun Coachmark(onDismiss: () -> Unit) { ) { Column(horizontalAlignment = Alignment.End) { Surface( - shape = RoundedCornerShape(16.dp), + shape = WonkySquircleShape.Toast, color = MaterialTheme.colorScheme.primary, shadowElevation = 6.dp, modifier = Modifier.width(260.dp), ) { Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -145,9 +148,11 @@ private fun Coachmark(onDismiss: () -> Unit) { ) Text( text = stringResource(Res.string.apk_inspect_coachmark_title), - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + ), color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold, ) } Text( @@ -156,14 +161,16 @@ private fun Coachmark(onDismiss: () -> Unit) { color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.9f), ) Row( - modifier = Modifier.padding(top = 4.dp).fillMaxWidthOnly(), + modifier = Modifier.padding(top = 2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.apk_inspect_coachmark_dismiss), color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), ) } } @@ -180,8 +187,6 @@ private fun Coachmark(onDismiss: () -> Unit) { } } -private fun Modifier.fillMaxWidthOnly(): Modifier = this.fillMaxWidth() - private fun Modifier.arrowDown(color: androidx.compose.ui.graphics.Color): Modifier = this.fillMaxSize().background( color = color, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt index d5386723f..c8f73b1dd 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt @@ -24,7 +24,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet +import zed.rainxch.core.presentation.vocabulary.Squiggle import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -54,7 +55,6 @@ fun LanguagePicker( ) { if (!isVisible) return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) var searchQuery by remember { mutableStateOf("") } val deviceLanguage = remember(deviceLanguageCode) { @@ -74,22 +74,16 @@ fun LanguagePicker( } } - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .navigationBarsPadding(), - ) { + GhsBottomSheet(onDismissRequest = onDismiss) { + Column(modifier = Modifier.fillMaxWidth()) { Text( text = stringResource(Res.string.translate_to), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(vertical = 6.dp), ) + Squiggle() + Spacer(Modifier.height(8.dp)) OutlinedTextField( value = searchQuery, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt index f086887b8..bce608e6e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import zed.rainxch.core.presentation.theme.tokens.Radii import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LinkOff import androidx.compose.material.icons.outlined.Link @@ -37,7 +38,7 @@ fun LinkedRepoBanner( ) { Surface( modifier = modifier, - shape = RoundedCornerShape(16.dp), + shape = Radii.row, color = MaterialTheme.colorScheme.surfaceContainerHighest, ) { Row( diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt index d35fc69aa..afa9b19eb 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt @@ -1,14 +1,16 @@ package zed.rainxch.details.presentation.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -21,21 +23,20 @@ import androidx.compose.material.icons.filled.UnfoldMore import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.outlined.Devices import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.sp import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -57,6 +58,10 @@ import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubAsset import zed.rainxch.core.domain.model.GithubUser import zed.rainxch.core.domain.util.AssetVariant +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.githubstore.core.presentation.res.Res @@ -107,38 +112,47 @@ fun ReleaseAssetsPicker( ) { Text( text = stringResource(Res.string.assets_title), - style = MaterialTheme.typography.labelLargeEmphasized, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.padding(horizontal = 4.dp), ) - OutlinedCard( - onClick = { onAction(DetailsAction.ToggleReleaseAssetsPicker) }, - enabled = isPickerEnabled, - modifier = Modifier.fillMaxWidth(), + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = Radii.row, + ) + .background(MaterialTheme.colorScheme.surface) + .clickable(enabled = isPickerEnabled) { + onAction(DetailsAction.ToggleReleaseAssetsPicker) + } + .padding(horizontal = 14.dp, vertical = 10.dp) + .heightIn(min = 36.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp) - .heightIn(min = 36.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = selectedAsset?.name ?: stringResource(Res.string.no_assets_selected), - style = MaterialTheme.typography.titleSmall, + Text( + text = selectedAsset?.name ?: stringResource(Res.string.no_assets_selected), + style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - modifier = Modifier.weight(1f), - ) - Icon( - imageVector = Icons.Default.UnfoldMore, - contentDescription = stringResource(Res.string.select_version), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + ), + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.Default.UnfoldMore, + contentDescription = stringResource(Res.string.select_version), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) } } } @@ -161,7 +175,6 @@ private fun ReleaseAssetsItemsPicker( ) { if (!showPicker) return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) var showInfoDialog by rememberSaveable { mutableStateOf(false) } ReleaseAssetsAboutDialog( @@ -169,29 +182,26 @@ private fun ReleaseAssetsItemsPicker( onDismiss = { showInfoDialog = false }, ) - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - modifier = modifier, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .navigationBarsPadding(), - ) { + GhsBottomSheet(onDismissRequest = onDismiss, modifier = modifier) { + Column(modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(Res.string.assets_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = - Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .weight(1f), - ) + Column(modifier = Modifier.weight(1f).padding(vertical = 6.dp)) { + Text( + text = stringResource(Res.string.assets_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Squiggle() + } IconButton(onClick = { showInfoDialog = true }) { - Icon(imageVector = Icons.Outlined.Info, contentDescription = stringResource(Res.string.icon_content_description_info)) + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = stringResource(Res.string.icon_content_description_info), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } @@ -215,38 +225,37 @@ private fun ReleaseAssetsItemsPicker( } } - Surface( - shape = RoundedCornerShape(20.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = - Modifier - .clickable(onClick = { onToggleShowAllPlatforms(!showAllPlatforms) }) - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Outlined.Devices, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.size(12.dp)) - Text( - text = stringResource(Res.string.show_all_platforms_label), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), - ) - androidx.compose.material3.Switch( - checked = showAllPlatforms, - onCheckedChange = onToggleShowAllPlatforms, + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .clip(Radii.row) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = Radii.row, ) - } + .background(MaterialTheme.colorScheme.surface) + .clickable(onClick = { onToggleShowAllPlatforms(!showAllPlatforms) }) + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Devices, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.size(12.dp)) + Text( + text = stringResource(Res.string.show_all_platforms_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + androidx.compose.material3.Switch( + checked = showAllPlatforms, + onCheckedChange = onToggleShowAllPlatforms, + ) } val groups = remember(crossPlatformAssets) { @@ -337,30 +346,37 @@ private fun ReleaseAssetsAboutDialog( onDismiss: () -> Unit, modifier: Modifier = Modifier, properties: DialogProperties = DialogProperties(), - containerColor: Color = AlertDialogDefaults.containerColor, - shape: Shape = AlertDialogDefaults.shape, ) { if (!showDialog) return BasicAlertDialog(onDismissRequest = onDismiss, modifier = modifier, properties = properties) { - Surface( - color = containerColor, - contentColor = contentColorFor(containerColor), - shape = shape, + Box( + modifier = Modifier + .clip(WonkySquircleShape.Dialog) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.5.dp, + color = MaterialTheme.colorScheme.outline, + shape = WonkySquircleShape.Dialog, + ) + .padding(24.dp), ) { - Column( - modifier = Modifier.padding(24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { + Column { Text( text = stringResource(Res.string.multiple_assets_info_dialog_title), - style = MaterialTheme.typography.headlineSmall, - color = AlertDialogDefaults.titleContentColor, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onSurface, ) + Spacer(Modifier.size(6.dp)) + Squiggle() + Spacer(Modifier.size(12.dp)) Text( text = stringResource(Res.string.multiple_assets_info_dialog_text), style = MaterialTheme.typography.bodyMedium, - color = AlertDialogDefaults.textContentColor, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleasesStatusCard.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleasesStatusCard.kt index edbe6a7ed..196b85fe7 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleasesStatusCard.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleasesStatusCard.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.githubstore.core.presentation.res.Res @@ -42,7 +43,7 @@ fun ReleasesStatusCard( onRetry: () -> Unit, modifier: Modifier = Modifier, ) { - OutlinedCard(modifier = modifier.fillMaxWidth()) { + OutlinedCard(modifier = modifier.fillMaxWidth(), shape = Radii.row) { Column( modifier = Modifier diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt index 86435c416..1d1616ab6 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/StatItem.kt @@ -1,15 +1,20 @@ package zed.rainxch.details.presentation.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.formatCount @Composable fun StatItem( @@ -26,31 +31,32 @@ fun StatItem( stat: Long, modifier: Modifier = Modifier, ) { - OutlinedCard( - modifier = modifier, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), + Column( + modifier = modifier + .clip(Radii.row) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = Radii.row) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 14.dp, vertical = 10.dp), ) { - Column( - modifier = Modifier.padding(12.dp), - ) { - Text( - text = label, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.outline, - maxLines = 1, - softWrap = false, - ) - - Text( - text = formatCount(stat), - style = MaterialTheme.typography.titleLarge, + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, + ) + Text( + text = formatCount(stat), + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.Black, - color = MaterialTheme.colorScheme.onBackground, - ) - } + fontSize = 22.sp, + letterSpacing = (-0.3).sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) } } @@ -60,39 +66,33 @@ fun TextStatItem( value: String, modifier: Modifier = Modifier, ) { - OutlinedCard( - modifier = modifier, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), + Column( + modifier = modifier + .clip(Radii.row) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = Radii.row) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 14.dp, vertical = 10.dp), ) { - Column( - modifier = Modifier.padding(12.dp), - ) { - Text( - text = label, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.outline, - maxLines = 1, - softWrap = false, - ) - - Text( - text = value, - style = MaterialTheme.typography.titleLarge, + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, + ) + Text( + text = value, + style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.Black, - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - softWrap = false, - ) - } + fontSize = 18.sp, + letterSpacing = (-0.3).sp, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + ) } } - -private fun formatCount(count: Long): String = - when { - count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0) - count >= 1_000 -> String.format("%.1fK", count / 1_000.0) - else -> count.toString() - } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt index 5b46ceb8b..b89aae25c 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionPicker.kt @@ -1,5 +1,7 @@ package zed.rainxch.details.presentation.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -8,7 +10,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -20,28 +21,29 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.UnfoldMore import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubRelease import zed.rainxch.core.domain.model.isEffectivelyPreRelease import zed.rainxch.core.domain.model.preReleaseLabel +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.latest_badge @@ -51,7 +53,7 @@ import zed.rainxch.githubstore.core.presentation.res.pre_release_badge import zed.rainxch.githubstore.core.presentation.res.select_version import zed.rainxch.githubstore.core.presentation.res.versions_title -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun VersionPicker( selectedRelease: GithubRelease?, @@ -70,104 +72,102 @@ fun VersionPicker( ) { Text( text = stringResource(Res.string.versions_title), - style = MaterialTheme.typography.labelLargeEmphasized, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + ), color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.padding(horizontal = 4.dp), ) - OutlinedCard( - onClick = { onAction(DetailsAction.ToggleVersionPicker) }, - enabled = isPickerEnabled, - modifier = Modifier.fillMaxWidth(), + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = Radii.row, + ) + .background(MaterialTheme.colorScheme.surface) + .clickable(enabled = isPickerEnabled) { + onAction(DetailsAction.ToggleVersionPicker) + } + .padding(horizontal = 14.dp, vertical = 10.dp) + .heightIn(min = 36.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp) - .heightIn(min = 36.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = - selectedRelease?.tagName - ?: stringResource(Res.string.no_version_selected), - style = MaterialTheme.typography.titleSmall, + Column(modifier = Modifier.weight(1f)) { + Text( + text = selectedRelease?.tagName + ?: stringResource(Res.string.no_version_selected), + style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Clip, - maxLines = 1, - ) - selectedRelease?.name?.let { name -> - if (name != selectedRelease.tagName) { - Text( - text = name, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + ), + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Clip, + maxLines = 1, + ) + selectedRelease?.name?.let { name -> + if (name != selectedRelease.tagName) { + Text( + text = name, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } - Icon( - imageVector = Icons.Default.UnfoldMore, - contentDescription = stringResource(Res.string.select_version), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) } + Icon( + imageVector = Icons.Default.UnfoldMore, + contentDescription = stringResource(Res.string.select_version), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) } } if (isPickerVisible) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - - ModalBottomSheet( - onDismissRequest = { onAction(DetailsAction.ToggleVersionPicker) }, - sheetState = sheetState, - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .navigationBarsPadding(), - ) { + GhsBottomSheet(onDismissRequest = { onAction(DetailsAction.ToggleVersionPicker) }) { + Text( + text = stringResource(Res.string.versions_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(vertical = 6.dp), + ) + Squiggle() + Spacer(Modifier.size(8.dp)) + if (filteredReleases.isEmpty()) { Text( - text = stringResource(Res.string.versions_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = stringResource(Res.string.not_available), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 16.dp), ) - - HorizontalDivider() - - if (filteredReleases.isEmpty()) { - Text( - text = stringResource(Res.string.not_available), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(16.dp), - ) - } else { - val latestReleaseId by remember(filteredReleases) { - derivedStateOf { filteredReleases.firstOrNull()?.id } - } - - LazyColumn( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(vertical = 8.dp), - ) { - items( - items = filteredReleases, - key = { it.id }, - ) { release -> - VersionListItem( - release = release, - isSelected = release.id == selectedRelease?.id, - isLatest = release.id == latestReleaseId, - onClick = { onAction(DetailsAction.SelectRelease(release)) }, - ) - } + } else { + val latestReleaseId by remember(filteredReleases) { + derivedStateOf { filteredReleases.firstOrNull()?.id } + } + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(vertical = 8.dp), + ) { + items(items = filteredReleases, key = { it.id }) { release -> + VersionListItem( + release = release, + isSelected = release.id == selectedRelease?.id, + isLatest = release.id == latestReleaseId, + onClick = { onAction(DetailsAction.SelectRelease(release)) }, + ) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + thickness = 0.5.dp, + ) } } } @@ -183,13 +183,13 @@ private fun VersionListItem( onClick: () -> Unit, ) { Row( - modifier = - Modifier - .fillMaxWidth() - .clickable( - onClickLabel = stringResource(Res.string.select_version), - onClick = onClick, - ).padding(horizontal = 16.dp, vertical = 12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + onClickLabel = stringResource(Res.string.select_version), + onClick = onClick, + ) + .padding(horizontal = 4.dp, vertical = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -200,45 +200,52 @@ private fun VersionListItem( ) { Text( text = release.tagName, - style = MaterialTheme.typography.titleSmall, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, - color = - if (isSelected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.SemiBold, + ), + color = if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface, ) if (isLatest) { - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primaryContainer, - ) { - Text( - text = stringResource(Res.string.latest_badge), - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } + Text( + text = stringResource(Res.string.latest_badge), + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 10.sp, + letterSpacing = 0.6.sp, + ), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(50), + ) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) } if (release.isEffectivelyPreRelease()) { - val specificLabel = release.preReleaseLabel() - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.tertiaryContainer, - ) { - Text( - text = specificLabel ?: stringResource(Res.string.pre_release_badge), - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } + Text( + text = specificLabel ?: stringResource(Res.string.pre_release_badge), + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 10.sp, + letterSpacing = 0.6.sp, + ), + color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(50), + ) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) } } - release.name?.let { name -> if (name != release.tagName) { Text( @@ -250,14 +257,14 @@ private fun VersionListItem( ) } } - Text( text = release.publishedAt.take(10), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - if (isSelected) { Spacer(Modifier.width(8.dp)) Icon( diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionTypePicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionTypePicker.kt index 0808e385f..0500289ba 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionTypePicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/VersionTypePicker.kt @@ -4,13 +4,12 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.chips.FilterChip import zed.rainxch.details.domain.model.ReleaseCategory import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.githubstore.core.presentation.res.Res @@ -31,18 +30,15 @@ fun VersionTypePicker( ) { items(ReleaseCategory.entries) { category -> FilterChip( - selected = category == selectedCategory, + label = stringResource( + when (category) { + ReleaseCategory.STABLE -> Res.string.category_stable + ReleaseCategory.PRE_RELEASE -> Res.string.category_pre_release + ReleaseCategory.ALL -> Res.string.category_all + }, + ), + active = category == selectedCategory, onClick = { onAction(DetailsAction.SelectReleaseCategory(category)) }, - label = { - Text( - text = - when (category) { - ReleaseCategory.STABLE -> stringResource(Res.string.category_stable) - ReleaseCategory.PRE_RELEASE -> stringResource(Res.string.category_pre_release) - ReleaseCategory.ALL -> stringResource(Res.string.category_all) - }, - ) - }, ) } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt index f5265809e..7fd376a75 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt @@ -8,7 +8,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Icon +import androidx.compose.ui.draw.clip import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -34,9 +41,12 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.vocabulary.Squiggle import com.mikepenz.markdown.compose.Markdown import com.mikepenz.markdown.model.ImageTransformer import com.mikepenz.markdown.model.rememberMarkdownState @@ -69,38 +79,40 @@ fun LazyListScope.about( onTranslateClick: () -> Unit, onLanguagePickerClick: () -> Unit, onToggleTranslation: () -> Unit, + onReadMore: (() -> Unit)? = null, ) { item { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) Row( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(Res.string.about_this_app), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, - ) - - readmeLanguage?.let { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( - text = it, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline, + text = stringResource(Res.string.about_this_app), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, ) + readmeLanguage?.let { + Text( + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, + ) + } } + Squiggle() } TranslationControls( @@ -110,6 +122,7 @@ fun LazyListScope.about( onToggleTranslation = onToggleTranslation, ) } + Spacer(Modifier.height(8.dp)) } item(key = "about_markdown") { @@ -131,15 +144,13 @@ fun LazyListScope.about( rawMarkdown = raw, isDark = isDark, isExpanded = isExpanded, - onToggleExpanded = onToggleExpanded, + onToggleExpanded = onReadMore ?: onToggleExpanded, imageTransformer = imageTransformer, collapsedHeight = collapsedHeight, measuredHeightPx = measuredHeightPx, onMeasured = onMeasured, fadeColor = MaterialTheme.colorScheme.background, - modifier = - Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) } } @@ -230,36 +241,56 @@ fun ExpandableMarkdownContent( if (!isExpanded && needsExpansion) { Box( - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(80.dp) - .background( - Brush.verticalGradient( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(140.dp) + .background( + Brush.verticalGradient( + colorStops = arrayOf( 0f to fadeColor.copy(alpha = 0f), + 0.35f to fadeColor.copy(alpha = 0.10f), + 0.6f to fadeColor.copy(alpha = 0.35f), + 0.8f to fadeColor.copy(alpha = 0.7f), 1f to fadeColor, ), ), + ), ) } } if (needsExpansion) { - TextButton( - onClick = onToggleExpanded, - modifier = Modifier.align(Alignment.CenterHorizontally), + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.onSurface) + .clickable(onClick = onToggleExpanded) + .padding(horizontal = 22.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = - if (isExpanded) { - stringResource(Res.string.show_less) - } else { - stringResource(Res.string.read_more) - }, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, + text = if (isExpanded) { + stringResource(Res.string.show_less) + } else { + stringResource(Res.string.read_more) + }, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.surface, ) + if (!isExpanded) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = MaterialTheme.colorScheme.surface, + modifier = Modifier.size(18.dp), + ) + } } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt index a74230eee..3a00da51d 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt @@ -13,8 +13,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.Update -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -57,12 +57,20 @@ fun LazyListScope.header( release = state.selectedRelease, repository = state.repository, installedApp = state.installedApp, + stats = state.stats, downloadStage = state.downloadStage, downloadProgress = state.downloadProgressPercent, isCurrentUserOwner = state.isCurrentUserOwner, onPlatformClick = { platform -> onAction(DetailsAction.OnPlatformChipClick(platform)) }, + onOwnerClick = { + onAction( + DetailsAction.OpenDeveloperProfile( + state.repository.owner.login, + ), + ) + }, ) } } @@ -195,96 +203,48 @@ fun LazyListScope.header( } } - DropdownMenu( + GhsDropdownMenu( expanded = state.isInstallDropdownExpanded, onDismissRequest = { onAction(DetailsAction.OnToggleInstallDropdown) }, offset = DpOffset(x = 0.dp, y = 20.dp), ) { - DropdownMenuItem( - text = { - Column { - Text( - text = stringResource(Res.string.open_in_obtainium), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.obtainium_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - }, - onClick = { - onAction(DetailsAction.OpenInObtainium) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.open_in_obtainium), + subtitle = stringResource(Res.string.obtainium_description), leadingIcon = { Icon( imageVector = Icons.Default.Update, contentDescription = null, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(20.dp), ) }, + onClick = { onAction(DetailsAction.OpenInObtainium) }, ) - - Spacer(Modifier.height(8.dp)) - - DropdownMenuItem( - text = { - Column { - Text( - text = stringResource(Res.string.inspect_with_appmanager), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.appmanager_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - }, - onClick = { - onAction(DetailsAction.OpenInAppManager) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.inspect_with_appmanager), + subtitle = stringResource(Res.string.appmanager_description), leadingIcon = { Icon( imageVector = Icons.Default.Security, contentDescription = null, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(20.dp), ) }, + onClick = { onAction(DetailsAction.OpenInAppManager) }, ) - - Spacer(Modifier.height(8.dp)) - - DropdownMenuItem( - text = { - Column { - Text( - text = stringResource(Res.string.open_with_external_installer), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.external_installer_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - }, - onClick = { - onAction(DetailsAction.InstallWithExternalApp) - }, + GhsDropdownMenuItem( + text = stringResource(Res.string.open_with_external_installer), + subtitle = stringResource(Res.string.external_installer_description), leadingIcon = { Icon( imageVector = Icons.AutoMirrored.Filled.OpenInNew, contentDescription = null, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(20.dp), ) }, + onClick = { onAction(DetailsAction.InstallWithExternalApp) }, ) } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt index 57079c9d5..abd4ffa54 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Logs.kt @@ -1,16 +1,22 @@ package zed.rainxch.details.presentation.components.sections +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.geistMono +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.details.presentation.DetailsState import zed.rainxch.details.presentation.model.LogResult import zed.rainxch.details.presentation.utils.asText @@ -18,15 +24,23 @@ import zed.rainxch.githubstore.core.presentation.res.* fun LazyListScope.logs(state: DetailsState) { item { - HorizontalDivider() - - Text( - text = stringResource(Res.string.install_logs), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(vertical = 8.dp), - fontWeight = FontWeight.Bold, - ) + Spacer(Modifier.height(20.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResource(Res.string.install_logs), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } } itemsIndexed( @@ -36,16 +50,15 @@ fun LazyListScope.logs(state: DetailsState) { ) { _, log -> Text( text = "> ${log.result.asText()}: ${log.assetName}", - style = - MaterialTheme.typography.labelSmall.copy( - fontStyle = FontStyle.Italic, - ), - color = - if (log.result is LogResult.Error) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.outline - }, + style = MaterialTheme.typography.labelSmall.copy( + fontFamily = geistMono, + fontSize = 11.sp, + ), + color = if (log.result is LogResult.Error) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.outline + }, ) } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt index 382e66a7e..72273e266 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt @@ -1,5 +1,7 @@ package zed.rainxch.details.presentation.components.sections +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -7,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -14,154 +17,167 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material3.CardDefaults +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.skydoves.landscapist.coil3.CoilImage import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubUserProfile +import zed.rainxch.core.presentation.theme.shapes.CornerRadii +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.githubstore.core.presentation.res.* +private val DeveloperCardShape = WonkySquircleShape( + topStart = CornerRadii(26.dp, 20.dp), + topEnd = CornerRadii(20.dp, 26.dp), + bottomEnd = CornerRadii(26.dp, 20.dp), + bottomStart = CornerRadii(20.dp, 26.dp), +) + @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.author( author: GithubUserProfile?, onAction: (DetailsAction) -> Unit, ) { item { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) - Text( - text = stringResource(Res.string.author), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(bottom = 12.dp), - fontWeight = FontWeight.Bold, - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResource(Res.string.details_developer_section), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } - OutlinedCard( - onClick = { - author?.login?.let { author -> - onAction( - DetailsAction.OpenDeveloperProfile( - author, - ), - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(DeveloperCardShape) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = DeveloperCardShape, + ) + .background(MaterialTheme.colorScheme.surface) + .clickable(enabled = author?.login != null) { + author?.login?.let { login -> + onAction(DetailsAction.OpenDeveloperProfile(login)) + } } - }, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(32.dp), + .padding(horizontal = 14.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, + CoilImage( + imageModel = { author?.avatarUrl }, + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape), + loading = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { CircularWavyProgressIndicator() } + }, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - CoilImage( - imageModel = { author?.avatarUrl }, - modifier = - Modifier - .size(80.dp) - .clip(CircleShape), - loading = { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularWavyProgressIndicator() - } - }, - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - author?.login?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - - author?.bio?.let { bio -> - Text( - text = bio, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, - maxLines = 2, - softWrap = false, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(Modifier.height(4.dp)) - - author?.htmlUrl?.let { - Row( - modifier = - Modifier.clickable { - onAction(DetailsAction.OpenAuthorInBrowser) - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - painter = painterResource(Res.drawable.ic_github), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - - Text( - text = stringResource(Res.string.profile), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - } + author?.login?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) } - - author?.login?.let { author -> - IconButton( - onClick = { - onAction(DetailsAction.OpenDeveloperProfile(author)) + author?.bio?.let { bio -> + Text( + text = bio, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + softWrap = true, + overflow = TextOverflow.Ellipsis, + ) + } + if (!author?.htmlUrl.isNullOrBlank()) { + Spacer(Modifier.height(2.dp)) + Row( + modifier = Modifier.clickable { + onAction(DetailsAction.OpenAuthorInBrowser) }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor = MaterialTheme.colorScheme.onSurface, - ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = stringResource(Res.string.open_developer_profile), - modifier = Modifier.size(24.dp), + painter = painterResource(Res.drawable.ic_github), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(Res.string.profile), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.primary, ) } } } + if (author?.login != null) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.onSurface) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(Res.string.details_view_developer_profile), + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.surface, + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.surface, + ) + } + } } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt index e88d571e0..771363f75 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt @@ -43,6 +43,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.details.presentation.DetailsState import zed.rainxch.githubstore.core.presentation.res.Res @@ -145,15 +147,15 @@ fun LazyListScope.releaseChannel( Card( colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, + containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.25f), contentColor = MaterialTheme.colorScheme.onErrorContainer, ), - shape = RoundedCornerShape(12.dp), + shape = Radii.row, modifier = Modifier.fillMaxWidth(), ) { Row( verticalAlignment = Alignment.Top, - modifier = Modifier.padding(12.dp), + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), ) { Icon( imageVector = Icons.Default.WarningAmber, @@ -187,10 +189,10 @@ fun LazyListScope.releaseChannel( containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, contentColor = MaterialTheme.colorScheme.onSurface, ), - shape = RoundedCornerShape(12.dp), + shape = Radii.row, modifier = Modifier.fillMaxWidth(), ) { - Column(modifier = Modifier.padding(12.dp)) { + Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = Icons.Default.Bolt, @@ -290,14 +292,14 @@ private fun ChannelChipCoachmark(onDismiss: () -> Unit) { onDismissRequest = onDismiss, ) { Surface( - shape = RoundedCornerShape(16.dp), + shape = WonkySquircleShape.Toast, color = MaterialTheme.colorScheme.primary, shadowElevation = 6.dp, modifier = Modifier.width(280.dp), ) { Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReportIssue.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReportIssue.kt index 05ce2de9b..deeea7768 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReportIssue.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReportIssue.kt @@ -1,69 +1,67 @@ package zed.rainxch.details.presentation.components.sections +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.open_github_link import zed.rainxch.githubstore.core.presentation.res.open_in_browser import zed.rainxch.githubstore.core.presentation.res.report_issue fun LazyListScope.reportIssue(repoUrl: String) { item { val uriHandler = LocalUriHandler.current - - OutlinedCard( - onClick = { - uriHandler.openUri("${repoUrl.trimEnd('/')}/issues") - }, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(32.dp), + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = Radii.row) + .background(MaterialTheme.colorScheme.surface) + .clickable { uriHandler.openUri("${repoUrl.trimEnd('/')}/issues") } + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Default.BugReport, - contentDescription = stringResource(Res.string.report_issue), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(32.dp), - ) - - Text( - text = stringResource(Res.string.report_issue), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), - ) - - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = stringResource(Res.string.open_in_browser), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } + Icon( + imageVector = Icons.Default.BugReport, + contentDescription = stringResource(Res.string.report_issue), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringResource(Res.string.report_issue), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = stringResource(Res.string.open_in_browser), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt index fe8422245..34afb1553 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Stats.kt @@ -1,14 +1,23 @@ package zed.rainxch.details.presentation.components.sections import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.vocabulary.Squiggle import zed.rainxch.details.domain.model.RepoStats import zed.rainxch.details.presentation.components.StatItem import zed.rainxch.details.presentation.components.TextStatItem @@ -18,24 +27,39 @@ fun LazyListScope.stats( repoStats: RepoStats, ) { item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResource(Res.string.details_stats_section), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { StatItem( label = stringResource(Res.string.forks), stat = repoStats.forks, modifier = Modifier.weight(1.5f), ) - StatItem( label = stringResource(Res.string.stars), stat = repoStats.stars, modifier = Modifier.weight(2f), ) - StatItem( label = stringResource(Res.string.issues), stat = repoStats.openIssues, @@ -43,18 +67,17 @@ fun LazyListScope.stats( ) } - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { StatItem( label = stringResource(Res.string.downloads), stat = repoStats.totalDownloads, modifier = Modifier.weight(1f), ) - TextStatItem( label = stringResource(Res.string.license), value = repoStats.license ?: stringResource(Res.string.license_none), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt index ec63cfa04..daed10b28 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt @@ -8,7 +8,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.ui.draw.clip +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Icon import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding @@ -33,10 +39,14 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Brush import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import kotlinx.coroutines.Dispatchers +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.Squiggle import kotlinx.coroutines.withContext import org.intellij.markdown.parser.MarkdownParser import com.mikepenz.markdown.model.rememberMarkdownState @@ -65,26 +75,29 @@ fun LazyListScope.whatsNew( onTranslateClick: () -> Unit, onLanguagePickerClick: () -> Unit, onToggleTranslation: () -> Unit, + onReadMore: (() -> Unit)? = null, ) { item { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) Row( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = stringResource(Res.string.whats_new), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, - ) + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(Res.string.whats_new), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } TranslationControls( translationState = translationState, @@ -94,39 +107,36 @@ fun LazyListScope.whatsNew( ) } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(10.dp)) - Card( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceContainerLow, + shape = Radii.row, + ) + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - release.tagName, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - - Text( - release.publishedAt.take(10), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } + Text( + text = release.tagName, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Text( + text = release.publishedAt.take(10), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, + ) } } @@ -138,7 +148,7 @@ fun LazyListScope.whatsNew( release = release, collapsedHeight = collapsedHeight, isExpanded = isExpanded, - onToggleExpanded = onToggleExpanded, + onToggleExpanded = onReadMore ?: onToggleExpanded, measuredHeightPx = measuredHeightPx, onMeasured = onMeasured, ) @@ -190,7 +200,7 @@ private fun ExpandableMarkdownContent( val components = remember(isDark, imageTransformer) { githubStoreMarkdownComponents(imageTransformer, isDark) } - val cardColor = MaterialTheme.colorScheme.surfaceContainerLow + val cardColor = MaterialTheme.colorScheme.background val collapsedHeightPx = with(density) { collapsedHeight.toPx() } val effectiveHeight = measuredHeightPx ?: 0f @@ -238,36 +248,56 @@ private fun ExpandableMarkdownContent( if (!isExpanded && needsExpansion) { Box( - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(80.dp) - .background( - Brush.verticalGradient( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(140.dp) + .background( + Brush.verticalGradient( + colorStops = arrayOf( 0f to cardColor.copy(alpha = 0f), + 0.35f to cardColor.copy(alpha = 0.10f), + 0.6f to cardColor.copy(alpha = 0.35f), + 0.8f to cardColor.copy(alpha = 0.7f), 1f to cardColor, ), ), + ), ) } } if (needsExpansion) { - TextButton( - onClick = onToggleExpanded, - modifier = Modifier.align(Alignment.CenterHorizontally), + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.onSurface) + .clickable(onClick = onToggleExpanded) + .padding(horizontal = 22.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = - if (isExpanded) { - stringResource(Res.string.show_less) - } else { - stringResource(Res.string.read_more) - }, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, + text = if (isExpanded) { + stringResource(Res.string.show_less) + } else { + stringResource(Res.string.read_more) + }, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.surface, ) + if (!isExpanded) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = MaterialTheme.colorScheme.surface, + modifier = Modifier.size(18.dp), + ) + } } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt index ed92afeb5..3698b2cb4 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownImageTransformer.kt @@ -32,6 +32,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.cd_image class MarkdownImageTransformer( private val probeClient: HttpClient, @@ -112,7 +115,7 @@ class MarkdownImageTransformer( return ImageData( painter = painter, modifier = inlineModifier, - contentDescription = "Image", + contentDescription = stringResource(Res.string.cd_image), contentScale = ContentScale.Fit, ) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownUtils.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownUtils.kt index 870084779..42651026b 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownUtils.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/utils/MarkdownUtils.kt @@ -51,7 +51,6 @@ fun rememberMarkdownTypography(): MarkdownTypography { ), quote = typography.bodyLarge.copy( - fontStyle = FontStyle.Italic, color = colorScheme.onSurfaceVariant, ), paragraph = typography.bodyLarge, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt new file mode 100644 index 000000000..30311e7a5 --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt @@ -0,0 +1,10 @@ +package zed.rainxch.details.presentation.whatsnew + +import zed.rainxch.core.domain.model.GithubRelease + +data class DetailsWhatsNewState( + val isLoading: Boolean = true, + val repoName: String = "", + val releases: List = emptyList(), + val errorMessage: String? = null, +) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt new file mode 100644 index 000000000..1f8d3c9b7 --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt @@ -0,0 +1,59 @@ +package zed.rainxch.details.presentation.whatsnew + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import zed.rainxch.details.domain.repository.DetailsRepository + +class DetailsWhatsNewViewModel( + private val repositoryId: Long, + private val owner: String, + private val repo: String, + private val sourceHost: String?, + private val detailsRepository: DetailsRepository, +) : ViewModel() { + + private val _state = MutableStateFlow(DetailsWhatsNewState()) + val state = _state.asStateFlow() + + init { + load() + } + + fun retry() { + load() + } + + private fun load() { + viewModelScope.launch { + _state.update { it.copy(isLoading = true, errorMessage = null) } + runCatching { + val resolved = if (owner.isNotBlank() && repo.isNotBlank()) { + detailsRepository.getRepositoryByOwnerAndName(owner, repo, sourceHost) + } else { + detailsRepository.getRepositoryById(repositoryId) + } + val releases = detailsRepository.getAllReleases( + owner = resolved.owner.login, + repo = resolved.name, + defaultBranch = resolved.defaultBranch, + sourceHost = sourceHost, + ) + resolved to releases + }.onSuccess { (resolved, releases) -> + _state.update { + it.copy( + isLoading = false, + repoName = resolved.name, + releases = releases, + ) + } + }.onFailure { e -> + _state.update { it.copy(isLoading = false, errorMessage = e.message ?: "Failed to load") } + } + } + } +} diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt new file mode 100644 index 000000000..ed1384833 --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt @@ -0,0 +1,196 @@ +package zed.rainxch.details.presentation.whatsnew + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mikepenz.markdown.compose.Markdown +import io.ktor.client.HttpClient +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named +import zed.rainxch.core.presentation.components.buttons.IconButton +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.vocabulary.Squiggle +import zed.rainxch.details.presentation.markdown.githubStoreMarkdownComponents +import zed.rainxch.details.presentation.utils.MarkdownImageTransformer +import zed.rainxch.details.presentation.utils.rememberMarkdownColors +import zed.rainxch.details.presentation.utils.rememberMarkdownTypography +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.cd_back +import zed.rainxch.githubstore.core.presentation.res.details_whats_new_screen_title +import zed.rainxch.githubstore.core.presentation.res.no_release_notes + +@Composable +fun WhatsNewRoot( + repositoryId: Long, + owner: String, + repo: String, + sourceHost: String?, + onNavigateBack: () -> Unit, + viewModel: DetailsWhatsNewViewModel = koinViewModel { + parametersOf(repositoryId, owner, repo, sourceHost) + }, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + WhatsNewScreen( + state = state, + onBack = onNavigateBack, + ) +} + +@Composable +private fun WhatsNewScreen( + state: DetailsWhatsNewState, + onBack: () -> Unit, +) { + val isDark = androidx.compose.foundation.isSystemInDarkTheme() + val probeClient = koinInject(qualifier = named("test")) + val imageTransformer = remember(probeClient) { MarkdownImageTransformer(probeClient) } + val colors = rememberMarkdownColors() + val typography = rememberMarkdownTypography() + val components = remember(isDark, imageTransformer) { + githubStoreMarkdownComponents(imageTransformer, isDark) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .systemBarsPadding(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.cd_back), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + Text( + text = state.repoName, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp), + ) + } + when { + state.isLoading -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { CircularProgressIndicator() } + + state.errorMessage != null -> Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = state.errorMessage, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } + + else -> LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + item(key = "header") { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(Res.string.details_whats_new_screen_title), + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 26.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } + } + + items(items = state.releases, key = { it.id }) { release -> + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = Radii.row, + ) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 14.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = release.tagName, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + ), + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = release.publishedAt.take(10), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(6.dp)) + val body = release.description?.takeIf { it.isNotBlank() } + ?: stringResource(Res.string.no_release_notes) + Markdown( + content = body, + colors = colors, + typography = typography, + imageTransformer = imageTransformer, + components = components, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } + } +} From 7982db5be9ef9177358903769c374a0908869cf9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:51:56 +0500 Subject: [PATCH 046/172] feat(nav): adaptive two-pane on Home, Search, Library --- .../githubstore/app/di/ViewModelsModule.kt | 22 +- .../navigation/AdaptiveDetailPaneContent.kt | 190 ++++++++++ .../app/navigation/AppNavigation.kt | 354 ++++++++++++++---- .../app/navigation/GithubStoreGraph.kt | 16 + 4 files changed, 508 insertions(+), 74 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AdaptiveDetailPaneContent.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index 3a586510d..9dc2efbd1 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -9,6 +9,8 @@ import zed.rainxch.apps.presentation.import.ExternalImportViewModel import zed.rainxch.apps.presentation.starred.StarredPickerViewModel import zed.rainxch.auth.presentation.AuthenticationViewModel import zed.rainxch.details.presentation.DetailsViewModel +import zed.rainxch.details.presentation.about.DetailsAboutViewModel +import zed.rainxch.details.presentation.whatsnew.DetailsWhatsNewViewModel import zed.rainxch.devprofile.presentation.DeveloperProfileViewModel import zed.rainxch.favourites.presentation.FavouritesViewModel import zed.rainxch.githubstore.app.announcements.AnnouncementsViewModel @@ -65,6 +67,24 @@ val viewModelsModule = systemInstallSerializer = get(), ) } + viewModel { params -> + DetailsAboutViewModel( + repositoryId = params[0], + owner = params[1], + repo = params[2], + sourceHost = if (params.size() > 3) params[3] else null, + detailsRepository = get(), + ) + } + viewModel { params -> + DetailsWhatsNewViewModel( + repositoryId = params[0], + owner = params[1], + repo = params[2], + sourceHost = if (params.size() > 3) params[3] else null, + detailsRepository = get(), + ) + } viewModelOf(::DeveloperProfileViewModel) viewModelOf(::FavouritesViewModel) viewModelOf(::HomeViewModel) @@ -85,7 +105,7 @@ val viewModelsModule = searchHistoryRepository = get(), hiddenReposRepository = get(), userSessionRepository = get(), - initialPlatform = get(), + initialPlatform = params.getOrNull(), ) } viewModelOf(::ProfileViewModel) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AdaptiveDetailPaneContent.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AdaptiveDetailPaneContent.kt new file mode 100644 index 000000000..09578c87f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AdaptiveDetailPaneContent.kt @@ -0,0 +1,190 @@ +package zed.rainxch.githubstore.app.navigation + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.navigation.NavHostController +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf +import zed.rainxch.core.presentation.components.adaptive.AdaptiveDetailArgs +import zed.rainxch.details.presentation.DetailsRoot +import zed.rainxch.details.presentation.DetailsViewModel +import zed.rainxch.details.presentation.about.AboutRoot +import zed.rainxch.details.presentation.whatsnew.WhatsNewRoot +import zed.rainxch.search.presentation.mappers.toSearchPlatformUi + +private sealed interface DetailPaneRoute { + data object Main : DetailPaneRoute + + data class About( + val repositoryId: Long, + val owner: String, + val repo: String, + val sourceHost: String?, + ) : DetailPaneRoute + + data class WhatsNew( + val repositoryId: Long, + val owner: String, + val repo: String, + val sourceHost: String?, + ) : DetailPaneRoute +} + +@Composable +fun AdaptiveDetailPaneContent( + args: AdaptiveDetailArgs, + navController: NavHostController, + onCrossNavToRepo: (AdaptiveDetailArgs) -> Unit, + onClearPane: () -> Unit, +) { + key(args) { + var route: DetailPaneRoute by remember { mutableStateOf(DetailPaneRoute.Main) } + + AnimatedContent( + targetState = route, + transitionSpec = { + val forward = targetState !is DetailPaneRoute.Main + if (forward) { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 280), + ) togetherWith + slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = tween(durationMillis = 280), + ) + } else { + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = tween(durationMillis = 280), + ) togetherWith + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 280), + ) + } + }, + label = "detail-pane-route", + ) { current -> + when (current) { + DetailPaneRoute.Main -> { + MainDetailPane( + args = args, + navController = navController, + onCrossNavToRepo = onCrossNavToRepo, + onClearPane = onClearPane, + onOpenAbout = { repoId, owner, repo, sourceHost -> + route = DetailPaneRoute.About(repoId, owner, repo, sourceHost) + }, + onOpenWhatsNew = { repoId, owner, repo, sourceHost -> + route = DetailPaneRoute.WhatsNew(repoId, owner, repo, sourceHost) + }, + ) + } + + is DetailPaneRoute.About -> { + val aboutKey = + "about|${current.repositoryId}|${current.owner}|${current.repo}|${current.sourceHost.orEmpty()}" + AboutRoot( + repositoryId = current.repositoryId, + owner = current.owner, + repo = current.repo, + sourceHost = current.sourceHost, + onNavigateBack = { route = DetailPaneRoute.Main }, + viewModel = + koinViewModel(key = aboutKey) { + parametersOf( + current.repositoryId, + current.owner, + current.repo, + current.sourceHost, + ) + }, + ) + } + + is DetailPaneRoute.WhatsNew -> { + val whatsNewKey = + "whatsnew|${current.repositoryId}|${current.owner}|${current.repo}|${current.sourceHost.orEmpty()}" + WhatsNewRoot( + repositoryId = current.repositoryId, + owner = current.owner, + repo = current.repo, + sourceHost = current.sourceHost, + onNavigateBack = { route = DetailPaneRoute.Main }, + viewModel = + koinViewModel(key = whatsNewKey) { + parametersOf( + current.repositoryId, + current.owner, + current.repo, + current.sourceHost, + ) + }, + ) + } + } + } + } +} + +@Composable +private fun MainDetailPane( + args: AdaptiveDetailArgs, + navController: NavHostController, + onCrossNavToRepo: (AdaptiveDetailArgs) -> Unit, + onClearPane: () -> Unit, + onOpenAbout: (Long, String, String, String?) -> Unit, + onOpenWhatsNew: (Long, String, String, String?) -> Unit, +) { + val vmKey = + buildString { + append(args.repositoryId) + append('|') + append(args.owner.orEmpty()) + append('|') + append(args.repo.orEmpty()) + append('|') + append(args.sourceHost.orEmpty()) + } + val viewModel: DetailsViewModel = + koinViewModel(key = vmKey) { + parametersOf( + args.repositoryId, + args.owner.orEmpty(), + args.repo.orEmpty(), + args.isComingFromUpdate, + args.sourceHost, + ) + } + DetailsRoot( + onNavigateBack = onClearPane, + onOpenRepositoryInApp = { repoId -> + onCrossNavToRepo(AdaptiveDetailArgs(repositoryId = repoId)) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, + onNavigateToSearchByPlatform = { platform -> + navController.navigate( + GithubStoreGraph.SearchScreen( + initialPlatform = platform.toSearchPlatformUi().name, + ), + ) + }, + onNavigateToAbout = onOpenAbout, + onNavigateToWhatsNew = onOpenWhatsNew, + viewModel = viewModel, + ) +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 5e55bbeb9..a81c89d56 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -26,6 +26,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.toRoute +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf import zed.rainxch.apps.presentation.AppsRoot @@ -36,21 +37,30 @@ import zed.rainxch.auth.presentation.AuthenticationRoot import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.Platform +import zed.rainxch.core.presentation.components.adaptive.AdaptiveDetailArgs +import zed.rainxch.core.presentation.components.adaptive.AdaptiveListDetailScaffold +import zed.rainxch.core.presentation.components.adaptive.rememberAdaptiveListDetailState import zed.rainxch.core.presentation.components.announcements.AnnouncementsRoot import zed.rainxch.core.presentation.components.whatsnew.WhatsNewHistoryScreen import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalContentWidth import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.details.presentation.DetailsRoot +import zed.rainxch.details.presentation.about.AboutRoot +import zed.rainxch.details.presentation.whatsnew.WhatsNewRoot import zed.rainxch.devprofile.presentation.DeveloperProfileRoot import zed.rainxch.favourites.presentation.FavouritesRoot import zed.rainxch.githubstore.app.announcements.AnnouncementsViewModel import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.adaptive_pick_repo_subtitle +import zed.rainxch.githubstore.core.presentation.res.adaptive_pick_repo_title import zed.rainxch.home.presentation.HomeRoot import zed.rainxch.profile.presentation.ProfileRoot import zed.rainxch.recentlyviewed.presentation.RecentlyViewedRoot import zed.rainxch.search.presentation.SearchRoot import zed.rainxch.search.presentation.mappers.toSearchPlatformUi +import zed.rainxch.search.presentation.model.SearchPlatformUi import zed.rainxch.starred.presentation.StarredReposRoot import zed.rainxch.tweaks.presentation.TweaksRoot import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot @@ -123,34 +133,58 @@ fun AppNavigation( modifier = Modifier.background(MaterialTheme.colorScheme.background), ) { composable { - HomeRoot( - onNavigateToSearch = { - navController.navigate(GithubStoreGraph.SearchScreen()) - }, - onNavigateToSettings = { - navController.navigate(GithubStoreGraph.ProfileScreen) - }, - onNavigateToApps = { - navController.navigate(GithubStoreGraph.AppsScreen) - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen(repositoryId = repoId), + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + HomeRoot( + onNavigateToSearch = { + navController.navigate(GithubStoreGraph.SearchScreen()) + }, + onNavigateToSettings = { + navController.navigate(GithubStoreGraph.ProfileScreen) + }, + onNavigateToApps = { + navController.navigate(GithubStoreGraph.AppsScreen) + }, + onNavigateToDetails = { repoId -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs(repositoryId = repoId), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen(repositoryId = repoId), + ) + } + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, + onNavigateToCategoryList = { category -> + navController.navigate( + GithubStoreGraph.CategoryListScreen(category.name), + ) + }, + onNavigateToStarredRepos = { + navController.navigate(GithubStoreGraph.StarredReposScreen) + }, ) }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen(username = username), - ) - }, - onNavigateToCategoryList = { category -> - navController.navigate( - GithubStoreGraph.CategoryListScreen(category.name), + detail = { args -> + AdaptiveDetailPaneContent( + args = args, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, ) }, - onNavigateToStarredRepos = { - navController.navigate(GithubStoreGraph.StarredReposScreen) - }, ) } @@ -179,41 +213,77 @@ fun AppNavigation( val initialPlatform = args.initialPlatform?.let { name -> runCatching { - zed.rainxch.search.presentation.model.SearchPlatformUi - .valueOf(name) + SearchPlatformUi.valueOf(name) }.getOrNull() } - SearchRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId, sourceHost -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - sourceHost = sourceHost, - ), - ) - }, - onNavigateToDetailsFromLink = { owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - owner = owner, - repo = repo, - ), + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + val searchViewModel: zed.rainxch.search.presentation.SearchViewModel = + koinViewModel { + parametersOf(initialPlatform) + } + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + SearchRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId, sourceHost -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + repositoryId = repoId, + sourceHost = sourceHost, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + sourceHost = sourceHost, + ), + ) + } + }, + onNavigateToDetailsFromLink = { owner, repo -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + owner = owner, + repo = repo, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + owner = owner, + repo = repo, + ), + ) + } + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + viewModel = searchViewModel, ) }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), + detail = { detailArgs -> + AdaptiveDetailPaneContent( + args = detailArgs, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, ) }, - viewModel = - koinViewModel { - parametersOf(initialPlatform) - }, ) } @@ -244,6 +314,26 @@ fun AppNavigation( ), ) }, + onNavigateToAbout = { repoId, owner, repo, sourceHost -> + navController.navigate( + GithubStoreGraph.DetailsAboutScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + sourceHost = sourceHost, + ), + ) + }, + onNavigateToWhatsNew = { repoId, owner, repo, sourceHost -> + navController.navigate( + GithubStoreGraph.DetailsWhatsNewScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + sourceHost = sourceHost, + ), + ) + }, viewModel = koinViewModel { parametersOf( @@ -257,6 +347,94 @@ fun AppNavigation( ) } + composable( + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + exitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + popEnterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + ) { backStackEntry -> + val args = backStackEntry.toRoute() + AboutRoot( + repositoryId = args.repositoryId, + owner = args.owner, + repo = args.repo, + sourceHost = args.sourceHost, + onNavigateBack = { navController.navigateUp() }, + ) + } + + composable( + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + exitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + popEnterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) + }, + ) { backStackEntry -> + val args = backStackEntry.toRoute() + WhatsNewRoot( + repositoryId = args.repositoryId, + owner = args.owner, + repo = args.repo, + sourceHost = args.sourceHost, + onNavigateBack = { navController.navigateUp() }, + ) + } + composable { backStackEntry -> val args = backStackEntry.toRoute() DeveloperProfileRoot( @@ -514,29 +692,59 @@ fun AppNavigation( appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) } } - AppsRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToRepo = { repoId, sourceHost, owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - isComingFromUpdate = true, - sourceHost = sourceHost, - owner = owner.orEmpty(), - repo = repo.orEmpty(), - ), + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + AppsRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToRepo = { repoId, sourceHost, owner, repo -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + repositoryId = repoId, + isComingFromUpdate = true, + sourceHost = sourceHost, + owner = owner, + repo = repo, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + isComingFromUpdate = true, + sourceHost = sourceHost, + owner = owner.orEmpty(), + repo = repo.orEmpty(), + ), + ) + } + }, + onNavigateToExternalImport = { + navController.navigate(GithubStoreGraph.ExternalImportScreen) + }, + onNavigateToStarredPicker = { + navController.navigate(GithubStoreGraph.StarredPickerScreen) + }, + viewModel = appsViewModel, + state = appsState, ) }, - onNavigateToExternalImport = { - navController.navigate(GithubStoreGraph.ExternalImportScreen) - }, - onNavigateToStarredPicker = { - navController.navigate(GithubStoreGraph.StarredPickerScreen) + detail = { detailArgs -> + AdaptiveDetailPaneContent( + args = detailArgs, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, + ) }, - viewModel = appsViewModel, - state = appsState, ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index da7d7f914..7015c30c7 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -78,4 +78,20 @@ sealed interface GithubStoreGraph { data class CategoryListScreen( val category: String, ) : GithubStoreGraph + + @Serializable + data class DetailsAboutScreen( + val repositoryId: Long = -1L, + val owner: String = "", + val repo: String = "", + val sourceHost: String? = null, + ) : GithubStoreGraph + + @Serializable + data class DetailsWhatsNewScreen( + val repositoryId: Long = -1L, + val owner: String = "", + val repo: String = "", + val sourceHost: String? = null, + ) : GithubStoreGraph } From 3030bb82ff8515462e376bf8d9a7fc69fc8ba492 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:52:16 +0500 Subject: [PATCH 047/172] chore: strip stale italic from chrome and onboarding --- .../rainxch/githubstore/app/navigation/BottomNavigation.kt | 1 - .../rainxch/githubstore/app/onboarding/OnboardingScreen.kt | 4 ---- 2 files changed, 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt index 7c9d6fd26..98041e71e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt @@ -156,7 +156,6 @@ private fun CookieTabItem( style = MaterialTheme.typography.labelSmall.copy( fontFamily = fraunces, - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, ), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index f09f36987..cf4ebf74e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -149,7 +149,6 @@ private fun StepPalette( style = MaterialTheme.typography.displaySmall.copy( fontFamily = fraunces, - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, @@ -274,7 +273,6 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { text = "G", color = MaterialTheme.colorScheme.onPrimary, fontFamily = fraunces, - fontStyle = FontStyle.Italic, fontWeight = FontWeight.Bold, fontSize = 48.sp, ) @@ -284,7 +282,6 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { style = MaterialTheme.typography.headlineSmall.copy( fontFamily = fraunces, - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, @@ -316,7 +313,6 @@ private fun StepPermissions(controller: OnboardingPermissionsController) { style = MaterialTheme.typography.headlineSmall.copy( fontFamily = fraunces, - fontStyle = FontStyle.Italic, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, From 9de318af085a4aa43412781630c79acd2197bd70 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:52:20 +0500 Subject: [PATCH 048/172] feat(search): scroll-collapsing topbar (wip) --- .../rainxch/search/presentation/SearchRoot.kt | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index f8cb19ff1..f8fae9e9e 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -1,6 +1,9 @@ package zed.rainxch.search.presentation import androidx.compose.animation.AnimatedVisibility +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically @@ -193,6 +196,26 @@ fun SearchScreen( val listState = rememberLazyStaggeredGridState() val bottomNavHeight = LocalBottomNavigationHeight.current + var showTopbar by remember { mutableStateOf(true) } + LaunchedEffect(listState) { + var prevIndex = 0 + var prevOffset = 0 + snapshotFlow { + listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset + }.collect { (index, offset) -> + val scrolledDown = index > prevIndex || (index == prevIndex && offset > prevOffset + 4) + val scrolledUp = index < prevIndex || (index == prevIndex && offset < prevOffset - 4) + showTopbar = when { + index == 0 && offset == 0 -> true + scrolledDown -> false + scrolledUp -> true + else -> showTopbar + } + prevIndex = index + prevOffset = offset + } + } + val shouldLoadMore by remember { derivedStateOf { val layoutInfo = listState.layoutInfo @@ -278,11 +301,17 @@ fun SearchScreen( Scaffold( topBar = { - SearchTopbar( - onAction = onAction, - state = state, - focusRequester = focusRequester, - ) + AnimatedVisibility( + visible = showTopbar, + enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(), + ) { + SearchTopbar( + onAction = onAction, + state = state, + focusRequester = focusRequester, + ) + } }, snackbarHost = { SnackbarHost( From 3261b42cf340d65e1a23adfd07ceaba94c82daff Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:52:48 +0500 Subject: [PATCH 049/172] chore(whatsnew): note design overhaul, two-pane, platform fix --- .../commonMain/composeResources/files/whatsnew/17.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json index 32898077a..f9f7b3e8f 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json @@ -13,7 +13,10 @@ "Search in Starred and Favourites — quickly filter long lists by repo name, owner, description, or language.", "Self-owned ✓ badge — when you're signed in, repos you own get a verified checkmark on Home and Search cards.", "Hide repository — long-press any repo card on Home or Search to hide it from discovery. The repo stays in your library if you have it installed.", - "Multi-OS release picker — toggle 'Show all platforms' on the asset picker to grab Android APKs from desktop or Linux .debs from your phone. Other-platform downloads open in your browser to save for transfer." + "Multi-OS release picker — toggle 'Show all platforms' on the asset picker to grab Android APKs from desktop or Linux .debs from your phone. Other-platform downloads open in your browser to save for transfer.", + "Design overhaul — new Geist typography, hero app header on Details, redesigned Home cards with platform glyphs, Apple-style menus, and a refreshed Library with an updates banner.", + "Tablet two-pane layout — Home, Search, and Library keep the list on the left and open the repo on the right; drag the divider to resize.", + "Inner Details pages — About and What's New now slide in as dedicated screens with their own back action." ] }, { @@ -24,7 +27,8 @@ "Updating multiple apps in a row now works reliably — installs serialize so each one shows the correct APK in the system dialog. Apps screen versions also stay in sync after every install.", "Auto-update picks the right APK when a release ships both a regular and an F-Droid variant — no more accidentally installing a sibling app with a different package id.", "Search no longer shows a result count over an empty list — when 'Hide seen' filters out every hit, the screen now explains why and offers a one-tap reset.", - "Sibling-app detection — auto-update no longer mistakes a different app shipped from the same repo (e.g. APP A → APP B) for a higher version of the tracked one." + "Sibling-app detection — auto-update no longer mistakes a different app shipped from the same repo (e.g. APP A → APP B) for a higher version of the tracked one.", + "Discovery cards now show every platform a repo ships installers for, not just the one whose endpoint listed it." ] }, { From b3a8a62eba79b8f246862e256250a1383aa0d328 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:54:56 +0500 Subject: [PATCH 050/172] revert(whatsnew): drop misplaced 1.9.0 bullets from 17.json --- .../commonMain/composeResources/files/whatsnew/17.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json index f9f7b3e8f..32898077a 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json @@ -13,10 +13,7 @@ "Search in Starred and Favourites — quickly filter long lists by repo name, owner, description, or language.", "Self-owned ✓ badge — when you're signed in, repos you own get a verified checkmark on Home and Search cards.", "Hide repository — long-press any repo card on Home or Search to hide it from discovery. The repo stays in your library if you have it installed.", - "Multi-OS release picker — toggle 'Show all platforms' on the asset picker to grab Android APKs from desktop or Linux .debs from your phone. Other-platform downloads open in your browser to save for transfer.", - "Design overhaul — new Geist typography, hero app header on Details, redesigned Home cards with platform glyphs, Apple-style menus, and a refreshed Library with an updates banner.", - "Tablet two-pane layout — Home, Search, and Library keep the list on the left and open the repo on the right; drag the divider to resize.", - "Inner Details pages — About and What's New now slide in as dedicated screens with their own back action." + "Multi-OS release picker — toggle 'Show all platforms' on the asset picker to grab Android APKs from desktop or Linux .debs from your phone. Other-platform downloads open in your browser to save for transfer." ] }, { @@ -27,8 +24,7 @@ "Updating multiple apps in a row now works reliably — installs serialize so each one shows the correct APK in the system dialog. Apps screen versions also stay in sync after every install.", "Auto-update picks the right APK when a release ships both a regular and an F-Droid variant — no more accidentally installing a sibling app with a different package id.", "Search no longer shows a result count over an empty list — when 'Hide seen' filters out every hit, the screen now explains why and offers a one-tap reset.", - "Sibling-app detection — auto-update no longer mistakes a different app shipped from the same repo (e.g. APP A → APP B) for a higher version of the tracked one.", - "Discovery cards now show every platform a repo ships installers for, not just the one whose endpoint listed it." + "Sibling-app detection — auto-update no longer mistakes a different app shipped from the same repo (e.g. APP A → APP B) for a higher version of the tracked one." ] }, { From c26bc10f34ac1ef8d641cd496079be51f67d18d6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:55:01 +0500 Subject: [PATCH 051/172] chore: bump to 1.9.0 versionCode 19 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c864b248c..2b5b5e3f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,8 +54,8 @@ ktlint-gradle = "12.1.1" flatpak-gradle-generator = "1.7.0" projectApplicationId = "zed.rainxch.githubstore" -projectVersionName = "1.8.3" -projectVersionCode = "18" +projectVersionName = "1.9.0" +projectVersionCode = "19" projectMinSdkVersion = "26" projectTargetSdkVersion = "36" projectCompileSdkVersion = "36" From 2afa5493744755002d86eb735da185e1f8fba2e3 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:55:05 +0500 Subject: [PATCH 052/172] feat(whatsnew): 1.9.0 design overhaul and two-pane --- .../composeResources/files/whatsnew/19.json | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 core/presentation/src/commonMain/composeResources/files/whatsnew/19.json diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/19.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/19.json new file mode 100644 index 000000000..ef62c5798 --- /dev/null +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/19.json @@ -0,0 +1,24 @@ +{ + "versionCode": 19, + "versionName": "1.9.0", + "releaseDate": "2026-05-23", + "showAsSheet": true, + "sections": [ + { + "type": "NEW", + "bullets": [ + "Design overhaul — new Geist typography, hero app header on Details, redesigned Home cards with platform glyphs, Apple-style menus, and a refreshed Library with an updates banner and a 'Ready to install' section.", + "Tablet two-pane layout — Home, Search, and Library keep the list on the left and open the repo on the right; drag the divider to resize.", + "Inner Details pages — About and What's New now slide in as dedicated screens with their own back action, and stay inside the right pane on tablet.", + "Real Apple and Tux icons for the macOS and Linux platform indicators across cards and lists." + ] + }, + { + "type": "FIXED", + "bullets": [ + "Discovery cards now show every platform a repo ships installers for, not just the one whose endpoint listed it.", + "What's New tag row no longer wraps long release tags into a one-character-wide vertical date column." + ] + } + ] +} From be53fe8c851311ca13d44cbe5d3454ebe1046208 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:57:38 +0500 Subject: [PATCH 053/172] fix(overlays): soften dropdown shadow 18dp to 6dp --- .../core/presentation/components/overlays/GhsDropdownMenu.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt index bc559167e..9c863cd48 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsDropdownMenu.kt @@ -56,7 +56,7 @@ fun GhsDropdownMenu( shape = RoundedCornerShape(14.dp), containerColor = MaterialTheme.colorScheme.surfaceContainer, tonalElevation = 0.dp, - shadowElevation = 18.dp, + shadowElevation = 6.dp, border = BorderStroke( width = 0.5.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.35f), From a8306fabb402eef6582898f0e00c180aed0486b0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 11:57:39 +0500 Subject: [PATCH 054/172] fix(apps): last-checked label uses onSurfaceVariant --- .../commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index b3042a76d..be1fc928c 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -601,7 +601,7 @@ fun AppsScreen( formatLastChecked(state.lastCheckedTimestamp), ), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), ) } From 15e883466d05aa390466654c31a28c7fbfa9ab35 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 12:09:37 +0500 Subject: [PATCH 055/172] feat(details): move translation into About and WhatsNew inner pages --- .../githubstore/app/di/ViewModelsModule.kt | 2 + .../composeResources/values/strings.xml | 14 + .../details/presentation/about/AboutRoot.kt | 48 ++- .../presentation/about/DetailsAboutState.kt | 5 + .../about/DetailsAboutViewModel.kt | 74 +++- .../components/TranslationCard.kt | 342 ++++++++++++++++++ .../presentation/components/sections/About.kt | 48 +-- .../components/sections/WhatsNew.kt | 30 +- .../whatsnew/DetailsWhatsNewState.kt | 4 + .../whatsnew/DetailsWhatsNewViewModel.kt | 73 +++- .../presentation/whatsnew/WhatsNewRoot.kt | 58 ++- 11 files changed, 639 insertions(+), 59 deletions(-) create mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index 9dc2efbd1..ce6af421a 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -74,6 +74,7 @@ val viewModelsModule = repo = params[2], sourceHost = if (params.size() > 3) params[3] else null, detailsRepository = get(), + translationRepository = get(), ) } viewModel { params -> @@ -83,6 +84,7 @@ val viewModelsModule = repo = params[2], sourceHost = if (params.size() > 3) params[3] else null, detailsRepository = get(), + translationRepository = get(), ) } viewModelOf(::DeveloperProfileViewModel) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 146bfa02a..0ba7ee171 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1169,6 +1169,20 @@ Advanced settings Pick a repository Tap any card on the left to view its details here. + Translate + Render this page in another language. + Translating… + Translated to %1$s + Showing original + Target language + Translate to %1$s + Translate + Show original + Show translation + Cancel + Retry + Detected source: %1$s + Change language APK Inspect diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt index fce75716f..a2c71f66c 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/AboutRoot.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mikepenz.markdown.compose.Markdown -import com.mikepenz.markdown.compose.elements.MarkdownText import io.ktor.client.HttpClient import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject @@ -38,6 +37,8 @@ import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import zed.rainxch.core.presentation.components.buttons.IconButton import zed.rainxch.core.presentation.vocabulary.Squiggle +import zed.rainxch.details.presentation.components.LanguagePicker +import zed.rainxch.details.presentation.components.TranslationCard import zed.rainxch.details.presentation.markdown.githubStoreMarkdownComponents import zed.rainxch.details.presentation.utils.MarkdownImageTransformer import zed.rainxch.details.presentation.utils.rememberMarkdownColors @@ -62,6 +63,11 @@ fun AboutRoot( state = state, onBack = onNavigateBack, onRetry = viewModel::retry, + onTranslate = viewModel::translate, + onToggleTranslation = viewModel::toggleTranslation, + onPickLanguage = viewModel::showLanguagePicker, + onDismissLanguagePicker = viewModel::dismissLanguagePicker, + onClearTranslation = viewModel::clearTranslation, ) } @@ -70,6 +76,11 @@ private fun AboutScreen( state: DetailsAboutState, onBack: () -> Unit, onRetry: () -> Unit, + onTranslate: (String) -> Unit, + onToggleTranslation: () -> Unit, + onPickLanguage: () -> Unit, + onDismissLanguagePicker: () -> Unit, + onClearTranslation: () -> Unit, ) { val isDark = androidx.compose.foundation.isSystemInDarkTheme() val probeClient = koinInject(qualifier = named("test")) @@ -80,6 +91,14 @@ private fun AboutScreen( githubStoreMarkdownComponents(imageTransformer, isDark) } + val displayedMarkdown = if ( + state.translation.isShowingTranslation && state.translation.translatedText != null + ) { + state.translation.translatedText + } else { + state.readmeMarkdown + } + Column( modifier = Modifier .fillMaxSize() @@ -119,7 +138,7 @@ private fun AboutScreen( else -> LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { item(key = "header") { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { @@ -142,9 +161,21 @@ private fun AboutScreen( } } } + item(key = "translation_card") { + Spacer(Modifier.height(4.dp)) + TranslationCard( + state = state.translation, + deviceLanguageCode = state.deviceLanguageCode, + onPickLanguage = onPickLanguage, + onTranslate = onTranslate, + onToggle = onToggleTranslation, + onCancel = onClearTranslation, + ) + } item(key = "markdown") { + Spacer(Modifier.height(4.dp)) Markdown( - content = state.readmeMarkdown, + content = displayedMarkdown, colors = colors, typography = typography, imageTransformer = imageTransformer, @@ -155,6 +186,17 @@ private fun AboutScreen( } } } + + LanguagePicker( + isVisible = state.isLanguagePickerVisible, + selectedLanguageCode = state.translation.targetLanguageCode ?: state.deviceLanguageCode, + deviceLanguageCode = state.deviceLanguageCode, + onLanguageSelected = { lang -> + onDismissLanguagePicker() + onTranslate(lang.code) + }, + onDismiss = onDismissLanguagePicker, + ) } @Composable diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt index c8448e41d..1d8fba7cb 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutState.kt @@ -1,9 +1,14 @@ package zed.rainxch.details.presentation.about +import zed.rainxch.details.presentation.model.TranslationState + data class DetailsAboutState( val isLoading: Boolean = true, val repoName: String = "", val readmeMarkdown: String = "", val readmeLanguage: String? = null, val errorMessage: String? = null, + val deviceLanguageCode: String = "en", + val translation: TranslationState = TranslationState(), + val isLanguagePickerVisible: Boolean = false, ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt index 964b895f8..69d734b0a 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/about/DetailsAboutViewModel.kt @@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import zed.rainxch.details.domain.repository.DetailsRepository +import zed.rainxch.details.domain.repository.TranslationRepository +import zed.rainxch.details.presentation.model.SupportedLanguages +import zed.rainxch.details.presentation.model.TranslationState class DetailsAboutViewModel( private val repositoryId: Long, @@ -14,9 +17,12 @@ class DetailsAboutViewModel( private val repo: String, private val sourceHost: String?, private val detailsRepository: DetailsRepository, + private val translationRepository: TranslationRepository, ) : ViewModel() { - private val _state = MutableStateFlow(DetailsAboutState()) + private val _state = MutableStateFlow( + DetailsAboutState(deviceLanguageCode = translationRepository.getDeviceLanguageCode()), + ) val state = _state.asStateFlow() init { @@ -27,6 +33,72 @@ class DetailsAboutViewModel( load() } + fun translate(targetLanguageCode: String) { + val markdown = _state.value.readmeMarkdown + if (markdown.isBlank()) return + val displayName = SupportedLanguages.all.firstOrNull { it.code == targetLanguageCode }?.displayName + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = true, + targetLanguageCode = targetLanguageCode, + targetLanguageDisplayName = displayName, + error = null, + ), + ) + } + viewModelScope.launch { + runCatching { + translationRepository.translate(markdown, targetLanguageCode) + }.onSuccess { result -> + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = false, + translatedText = result.translatedText, + isShowingTranslation = true, + detectedSourceLanguage = result.detectedSourceLanguage, + error = null, + ), + ) + } + }.onFailure { e -> + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = false, + error = e.message ?: "Translation failed", + ), + ) + } + } + } + } + + fun toggleTranslation() { + _state.update { + it.copy( + translation = it.translation.copy( + isShowingTranslation = !it.translation.isShowingTranslation, + ), + ) + } + } + + fun clearTranslation() { + _state.update { + it.copy(translation = TranslationState()) + } + } + + fun showLanguagePicker() { + _state.update { it.copy(isLanguagePickerVisible = true) } + } + + fun dismissLanguagePicker() { + _state.update { it.copy(isLanguagePickerVisible = false) } + } + private fun load() { viewModelScope.launch { _state.update { it.copy(isLoading = true, errorMessage = null) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt new file mode 100644 index 000000000..816f40c63 --- /dev/null +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt @@ -0,0 +1,342 @@ +package zed.rainxch.details.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material.icons.outlined.Translate +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.details.presentation.model.SupportedLanguages +import zed.rainxch.details.presentation.model.TranslationState +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.translation_card_cancel +import zed.rainxch.githubstore.core.presentation.res.translation_card_change_language +import zed.rainxch.githubstore.core.presentation.res.translation_card_detected_source +import zed.rainxch.githubstore.core.presentation.res.translation_card_retry +import zed.rainxch.githubstore.core.presentation.res.translation_card_show_original +import zed.rainxch.githubstore.core.presentation.res.translation_card_show_translation +import zed.rainxch.githubstore.core.presentation.res.translation_card_subtitle_idle +import zed.rainxch.githubstore.core.presentation.res.translation_card_subtitle_original +import zed.rainxch.githubstore.core.presentation.res.translation_card_subtitle_translated +import zed.rainxch.githubstore.core.presentation.res.translation_card_subtitle_translating +import zed.rainxch.githubstore.core.presentation.res.translation_card_target_label +import zed.rainxch.githubstore.core.presentation.res.translation_card_title +import zed.rainxch.githubstore.core.presentation.res.translation_card_translate_to + +@Composable +fun TranslationCard( + state: TranslationState, + deviceLanguageCode: String, + onPickLanguage: () -> Unit, + onTranslate: (String) -> Unit, + onToggle: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + val effectiveTargetCode = state.targetLanguageCode ?: deviceLanguageCode + val effectiveTargetName = state.targetLanguageDisplayName + ?: SupportedLanguages.all.firstOrNull { it.code == effectiveTargetCode }?.displayName + ?: effectiveTargetCode + + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Header(state = state, displayName = effectiveTargetName) + + Spacer(Modifier.height(14.dp)) + + TargetLanguageRow( + displayName = effectiveTargetName, + onPick = onPickLanguage, + ) + + Spacer(Modifier.height(12.dp)) + + ActionRow( + state = state, + effectiveTargetCode = effectiveTargetCode, + effectiveTargetName = effectiveTargetName, + onTranslate = onTranslate, + onToggle = onToggle, + onCancel = onCancel, + ) + + if (state.error != null) { + Spacer(Modifier.height(10.dp)) + ErrorRow( + message = state.error, + onRetry = { onTranslate(effectiveTargetCode) }, + ) + } + + if (state.translatedText != null && state.detectedSourceLanguage != null) { + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource( + Res.string.translation_card_detected_source, + SupportedLanguages.all.firstOrNull { it.code == state.detectedSourceLanguage }?.displayName + ?: state.detectedSourceLanguage, + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun Header(state: TranslationState, displayName: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Translate, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(Res.string.translation_card_title), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 17.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + val subtitle = when { + state.isTranslating -> stringResource(Res.string.translation_card_subtitle_translating) + state.translatedText != null && state.isShowingTranslation -> + stringResource(Res.string.translation_card_subtitle_translated, displayName) + state.translatedText != null && !state.isShowingTranslation -> + stringResource(Res.string.translation_card_subtitle_original) + else -> stringResource(Res.string.translation_card_subtitle_idle) + } + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun TargetLanguageRow( + displayName: String, + onPick: () -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = stringResource(Res.string.translation_card_target_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = onPick), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Language, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(10.dp)) + Text( + text = displayName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = stringResource(Res.string.translation_card_change_language), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + } + } +} + +@Composable +private fun ActionRow( + state: TranslationState, + effectiveTargetCode: String, + effectiveTargetName: String, + onTranslate: (String) -> Unit, + onToggle: () -> Unit, + onCancel: () -> Unit, +) { + when { + state.isTranslating -> { + OutlinedButton( + onClick = onCancel, + modifier = Modifier + .fillMaxWidth() + .height(46.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(10.dp)) + Text(stringResource(Res.string.translation_card_cancel), fontWeight = FontWeight.SemiBold) + } + } + + state.translatedText != null -> { + OutlinedButton( + onClick = onToggle, + modifier = Modifier + .fillMaxWidth() + .height(46.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + Text( + text = if (state.isShowingTranslation) { + stringResource(Res.string.translation_card_show_original) + } else { + stringResource(Res.string.translation_card_show_translation) + }, + fontWeight = FontWeight.SemiBold, + ) + } + } + + else -> { + Button( + onClick = { onTranslate(effectiveTargetCode) }, + modifier = Modifier + .fillMaxWidth() + .height(46.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Outlined.Translate, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource( + Res.string.translation_card_translate_to, + effectiveTargetName, + ), + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +private fun ErrorRow( + message: String, + onRetry: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.errorContainer, + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.width(8.dp)) + Box( + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.error) + .clickable(onClick = onRetry) + .padding(horizontal = 12.dp, vertical = 6.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(Res.string.translation_card_retry), + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onError, + ) + } + } + } +} diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt index 7fd376a75..b66da43ca 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt @@ -84,43 +84,33 @@ fun LazyListScope.about( item { Spacer(Modifier.height(20.dp)) - Row( + Column( modifier = Modifier .fillMaxWidth() .padding(bottom = 6.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(Res.string.about_this_app), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + readmeLanguage?.let { Text( - text = stringResource(Res.string.about_this_app), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - ), - color = MaterialTheme.colorScheme.onBackground, + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, ) - readmeLanguage?.let { - Text( - text = it, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline, - ) - } } - Squiggle() } - - TranslationControls( - translationState = translationState, - onTranslateClick = onTranslateClick, - onLanguagePickerClick = onLanguagePickerClick, - onToggleTranslation = onToggleTranslation, - ) + Squiggle() } Spacer(Modifier.height(8.dp)) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt index daed10b28..edcd8f47f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt @@ -80,31 +80,21 @@ fun LazyListScope.whatsNew( item { Spacer(Modifier.height(20.dp)) - Row( + Column( modifier = Modifier .fillMaxWidth() .padding(bottom = 6.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = stringResource(Res.string.whats_new), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - ), - color = MaterialTheme.colorScheme.onBackground, - ) - Squiggle() - } - - TranslationControls( - translationState = translationState, - onTranslateClick = onTranslateClick, - onLanguagePickerClick = onLanguagePickerClick, - onToggleTranslation = onToggleTranslation, + Text( + text = stringResource(Res.string.whats_new), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, ) + Squiggle() } Spacer(Modifier.height(10.dp)) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt index 30311e7a5..a0d81ce0f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewState.kt @@ -1,10 +1,14 @@ package zed.rainxch.details.presentation.whatsnew import zed.rainxch.core.domain.model.GithubRelease +import zed.rainxch.details.presentation.model.TranslationState data class DetailsWhatsNewState( val isLoading: Boolean = true, val repoName: String = "", val releases: List = emptyList(), val errorMessage: String? = null, + val deviceLanguageCode: String = "en", + val translation: TranslationState = TranslationState(), + val isLanguagePickerVisible: Boolean = false, ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt index 1f8d3c9b7..385cbd2b1 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/DetailsWhatsNewViewModel.kt @@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import zed.rainxch.details.domain.repository.DetailsRepository +import zed.rainxch.details.domain.repository.TranslationRepository +import zed.rainxch.details.presentation.model.SupportedLanguages +import zed.rainxch.details.presentation.model.TranslationState class DetailsWhatsNewViewModel( private val repositoryId: Long, @@ -14,9 +17,12 @@ class DetailsWhatsNewViewModel( private val repo: String, private val sourceHost: String?, private val detailsRepository: DetailsRepository, + private val translationRepository: TranslationRepository, ) : ViewModel() { - private val _state = MutableStateFlow(DetailsWhatsNewState()) + private val _state = MutableStateFlow( + DetailsWhatsNewState(deviceLanguageCode = translationRepository.getDeviceLanguageCode()), + ) val state = _state.asStateFlow() init { @@ -27,6 +33,71 @@ class DetailsWhatsNewViewModel( load() } + fun translate(targetLanguageCode: String) { + val releases = _state.value.releases + val body = releases.firstOrNull()?.description.orEmpty() + if (body.isBlank()) return + val displayName = SupportedLanguages.all.firstOrNull { it.code == targetLanguageCode }?.displayName + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = true, + targetLanguageCode = targetLanguageCode, + targetLanguageDisplayName = displayName, + error = null, + ), + ) + } + viewModelScope.launch { + runCatching { + translationRepository.translate(body, targetLanguageCode) + }.onSuccess { result -> + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = false, + translatedText = result.translatedText, + isShowingTranslation = true, + detectedSourceLanguage = result.detectedSourceLanguage, + error = null, + ), + ) + } + }.onFailure { e -> + _state.update { + it.copy( + translation = it.translation.copy( + isTranslating = false, + error = e.message ?: "Translation failed", + ), + ) + } + } + } + } + + fun toggleTranslation() { + _state.update { + it.copy( + translation = it.translation.copy( + isShowingTranslation = !it.translation.isShowingTranslation, + ), + ) + } + } + + fun clearTranslation() { + _state.update { it.copy(translation = TranslationState()) } + } + + fun showLanguagePicker() { + _state.update { it.copy(isLanguagePickerVisible = true) } + } + + fun dismissLanguagePicker() { + _state.update { it.copy(isLanguagePickerVisible = false) } + } + private fun load() { viewModelScope.launch { _state.update { it.copy(isLoading = true, errorMessage = null) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt index ed1384833..201f84042 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt @@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.CircularProgressIndicator @@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -41,6 +42,8 @@ import org.koin.core.qualifier.named import zed.rainxch.core.presentation.components.buttons.IconButton import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.vocabulary.Squiggle +import zed.rainxch.details.presentation.components.LanguagePicker +import zed.rainxch.details.presentation.components.TranslationCard import zed.rainxch.details.presentation.markdown.githubStoreMarkdownComponents import zed.rainxch.details.presentation.utils.MarkdownImageTransformer import zed.rainxch.details.presentation.utils.rememberMarkdownColors @@ -65,6 +68,11 @@ fun WhatsNewRoot( WhatsNewScreen( state = state, onBack = onNavigateBack, + onTranslate = viewModel::translate, + onToggleTranslation = viewModel::toggleTranslation, + onPickLanguage = viewModel::showLanguagePicker, + onDismissLanguagePicker = viewModel::dismissLanguagePicker, + onClearTranslation = viewModel::clearTranslation, ) } @@ -72,6 +80,11 @@ fun WhatsNewRoot( private fun WhatsNewScreen( state: DetailsWhatsNewState, onBack: () -> Unit, + onTranslate: (String) -> Unit, + onToggleTranslation: () -> Unit, + onPickLanguage: () -> Unit, + onDismissLanguagePicker: () -> Unit, + onClearTranslation: () -> Unit, ) { val isDark = androidx.compose.foundation.isSystemInDarkTheme() val probeClient = koinInject(qualifier = named("test")) @@ -148,7 +161,18 @@ private fun WhatsNewScreen( } } - items(items = state.releases, key = { it.id }) { release -> + item(key = "translation_card") { + TranslationCard( + state = state.translation, + deviceLanguageCode = state.deviceLanguageCode, + onPickLanguage = onPickLanguage, + onTranslate = onTranslate, + onToggle = onToggleTranslation, + onCancel = onClearTranslation, + ) + } + + itemsIndexed(items = state.releases, key = { _, item -> item.id }) { index, release -> Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Row( modifier = Modifier @@ -161,7 +185,7 @@ private fun WhatsNewScreen( ) .background(MaterialTheme.colorScheme.surface) .padding(horizontal = 14.dp, vertical = 10.dp), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -170,16 +194,29 @@ private fun WhatsNewScreen( fontWeight = FontWeight.Bold, ), color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), ) Text( text = release.publishedAt.take(10), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, ) } Spacer(Modifier.height(6.dp)) - val body = release.description?.takeIf { it.isNotBlank() } - ?: stringResource(Res.string.no_release_notes) + val isLatest = index == 0 + val useTranslated = isLatest && + state.translation.isShowingTranslation && + state.translation.translatedText != null + val body = if (useTranslated) { + state.translation.translatedText!! + } else { + release.description?.takeIf { it.isNotBlank() } + ?: stringResource(Res.string.no_release_notes) + } Markdown( content = body, colors = colors, @@ -193,4 +230,15 @@ private fun WhatsNewScreen( } } } + + LanguagePicker( + isVisible = state.isLanguagePickerVisible, + selectedLanguageCode = state.translation.targetLanguageCode ?: state.deviceLanguageCode, + deviceLanguageCode = state.deviceLanguageCode, + onLanguageSelected = { lang -> + onDismissLanguagePicker() + onTranslate(lang.code) + }, + onDismiss = onDismissLanguagePicker, + ) } From 43d68aa82bc066ed42ab6c183f48378a4690b441 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 12:58:35 +0500 Subject: [PATCH 056/172] fix(home/lead): boost light-theme contrast and warmth --- .../home/presentation/components/LeadCard.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 934a0c53c..0120a16bb 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -67,6 +67,9 @@ fun LeadCard( val isDark = isSystemInDarkTheme() val surface = MaterialTheme.colorScheme.surface val borderColor = if (isDark) EmberPalette.Hot.copy(alpha = 0.42f) else EmberPalette.Deep.copy(alpha = 0.5f) + val ownerAlpha = if (isDark) 0.7f else 0.85f + val descAlpha = if (isDark) 0.78f else 0.92f + val statAlpha = if (isDark) 0.88f else 1f Box( modifier = modifier @@ -77,8 +80,8 @@ fun LeadCard( .drawBehind { val warmth = Brush.linearGradient( colorStops = arrayOf( - 0f to EmberPalette.Hot.copy(alpha = if (isDark) 0.32f else 0.16f), - 0.6f to EmberPalette.Warm.copy(alpha = if (isDark) 0.18f else 0.08f), + 0f to EmberPalette.Hot.copy(alpha = if (isDark) 0.32f else 0.22f), + 0.6f to EmberPalette.Warm.copy(alpha = if (isDark) 0.18f else 0.13f), 1f to Color.Transparent, ), start = Offset(0f, 0f), @@ -87,7 +90,7 @@ fun LeadCard( drawRect(brush = warmth) val sun = Brush.radialGradient( colors = listOf( - EmberPalette.Amber.copy(alpha = if (isDark) 0.18f else 0.12f), + EmberPalette.Amber.copy(alpha = if (isDark) 0.18f else 0.17f), Color.Transparent, ), center = Offset(size.width * 0.18f, size.height * 0.25f), @@ -130,7 +133,7 @@ fun LeadCard( style = MaterialTheme.typography.labelMedium.copy( fontSize = 12.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = ownerAlpha), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), @@ -186,7 +189,7 @@ fun LeadCard( Text( text = card.description, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 13.sp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.78f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = descAlpha), maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -204,7 +207,7 @@ fun LeadCard( Icon( imageVector = Icons.Outlined.Star, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), modifier = Modifier.size(15.dp), ) Text( @@ -213,7 +216,7 @@ fun LeadCard( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), ) } if (card.downloadsCount > 0) { @@ -224,7 +227,7 @@ fun LeadCard( Icon( imageVector = Icons.Default.Download, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), modifier = Modifier.size(15.dp), ) Text( @@ -233,7 +236,7 @@ fun LeadCard( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.88f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), ) } } From d8bb21ba3c957cc92de340906861398af9c48c09 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:07:21 +0500 Subject: [PATCH 057/172] fix(home/lead): use Ember Ash ink on light theme, drop green clash --- .../home/presentation/components/LeadCard.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 0120a16bb..26befc8ae 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -67,9 +67,10 @@ fun LeadCard( val isDark = isSystemInDarkTheme() val surface = MaterialTheme.colorScheme.surface val borderColor = if (isDark) EmberPalette.Hot.copy(alpha = 0.42f) else EmberPalette.Deep.copy(alpha = 0.5f) - val ownerAlpha = if (isDark) 0.7f else 0.85f - val descAlpha = if (isDark) 0.78f else 0.92f - val statAlpha = if (isDark) 0.88f else 1f + val inkColor = if (isDark) MaterialTheme.colorScheme.onSurface else EmberPalette.Ash + val ownerAlpha = if (isDark) 0.7f else 0.78f + val descAlpha = if (isDark) 0.78f else 0.9f + val statAlpha = if (isDark) 0.88f else 0.95f Box( modifier = modifier @@ -133,7 +134,7 @@ fun LeadCard( style = MaterialTheme.typography.labelMedium.copy( fontSize = 12.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = ownerAlpha), + color = inkColor.copy(alpha = ownerAlpha), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), @@ -178,7 +179,7 @@ fun LeadCard( fontSize = 28.sp, letterSpacing = (-0.3).sp, ), - color = MaterialTheme.colorScheme.onSurface, + color = inkColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -189,7 +190,7 @@ fun LeadCard( Text( text = card.description, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 13.sp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = descAlpha), + color = inkColor.copy(alpha = descAlpha), maxLines = 2, overflow = TextOverflow.Ellipsis, ) @@ -207,7 +208,7 @@ fun LeadCard( Icon( imageVector = Icons.Outlined.Star, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), + tint = inkColor.copy(alpha = statAlpha), modifier = Modifier.size(15.dp), ) Text( @@ -216,7 +217,7 @@ fun LeadCard( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), + color = inkColor.copy(alpha = statAlpha), ) } if (card.downloadsCount > 0) { @@ -227,7 +228,7 @@ fun LeadCard( Icon( imageVector = Icons.Default.Download, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), + tint = inkColor.copy(alpha = statAlpha), modifier = Modifier.size(15.dp), ) Text( @@ -236,7 +237,7 @@ fun LeadCard( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = statAlpha), + color = inkColor.copy(alpha = statAlpha), ) } } From d7171d41405787e91d002068d13e84bfcacace87 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:13:05 +0500 Subject: [PATCH 058/172] fix(home/lead): solid Ember Ash bg, white ink, theme-agnostic --- .../home/presentation/components/LeadCard.kt | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt index 26befc8ae..3aaa7a801 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/LeadCard.kt @@ -64,25 +64,23 @@ fun LeadCard( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val isDark = isSystemInDarkTheme() - val surface = MaterialTheme.colorScheme.surface - val borderColor = if (isDark) EmberPalette.Hot.copy(alpha = 0.42f) else EmberPalette.Deep.copy(alpha = 0.5f) - val inkColor = if (isDark) MaterialTheme.colorScheme.onSurface else EmberPalette.Ash - val ownerAlpha = if (isDark) 0.7f else 0.78f - val descAlpha = if (isDark) 0.78f else 0.9f - val statAlpha = if (isDark) 0.88f else 0.95f + val borderColor = EmberPalette.Hot.copy(alpha = 0.55f) + val inkColor = Color.White + val ownerAlpha = 0.78f + val descAlpha = 0.92f + val statAlpha = 0.95f Box( modifier = modifier .fillMaxWidth() .height(248.dp) .clip(LeadShape) - .background(if (isDark) EmberPalette.Ash else surface) + .background(EmberPalette.Ash) .drawBehind { val warmth = Brush.linearGradient( colorStops = arrayOf( - 0f to EmberPalette.Hot.copy(alpha = if (isDark) 0.32f else 0.22f), - 0.6f to EmberPalette.Warm.copy(alpha = if (isDark) 0.18f else 0.13f), + 0f to EmberPalette.Hot.copy(alpha = 0.55f), + 0.6f to EmberPalette.Warm.copy(alpha = 0.28f), 1f to Color.Transparent, ), start = Offset(0f, 0f), @@ -91,7 +89,7 @@ fun LeadCard( drawRect(brush = warmth) val sun = Brush.radialGradient( colors = listOf( - EmberPalette.Amber.copy(alpha = if (isDark) 0.18f else 0.17f), + EmberPalette.Amber.copy(alpha = 0.22f), Color.Transparent, ), center = Offset(size.width * 0.18f, size.height * 0.25f), @@ -113,7 +111,7 @@ fun LeadCard( modifier = Modifier .size(76.dp) .clip(CircleShape) - .background(if (isDark) EmberPalette.Ash else surface) + .background(EmberPalette.Ash) .border(2.5.dp, EmberPalette.Deep, CircleShape), contentAlignment = Alignment.Center, ) { From ab88a414514e053efb3f5cde29f4dee4bcad8554 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:21:02 +0500 Subject: [PATCH 059/172] fix(details/topbar): swap share to browser, ripple-clip pills --- .../presentation/components/FloatingPill.kt | 8 ++++++-- .../details/presentation/DetailsRoot.kt | 18 +++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt index 76a659927..40a3028b4 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/FloatingPill.kt @@ -2,6 +2,7 @@ package zed.rainxch.core.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,19 +17,22 @@ import androidx.compose.ui.unit.dp @Composable fun FloatingPill( modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, horizontalPadding: Dp = 12.dp, verticalPadding: Dp = 10.dp, content: @Composable () -> Unit, ) { + val shape = RoundedCornerShape(50) Box( modifier = modifier - .clip(RoundedCornerShape(50)) + .clip(shape) .background(MaterialTheme.colorScheme.surface) .border( width = 1.dp, color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(50), + shape = shape, ) + .let { if (onClick != null) it.clickable(onClick = onClick) else it } .padding(horizontal = horizontalPadding, vertical = verticalPadding), contentAlignment = Alignment.Center, ) { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index 34764bf9a..7f314356f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -803,9 +803,7 @@ private fun DetailsTopbar( horizontalArrangement = Arrangement.SpaceBetween, ) { FloatingPill( - modifier = Modifier.clickable { - onAction(DetailsAction.OnNavigateBackClick) - }, + onClick = { onAction(DetailsAction.OnNavigateBackClick) }, ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, @@ -828,12 +826,13 @@ private fun DetailsTopbar( ) { Box( modifier = Modifier - .clickable { onAction(DetailsAction.OnShareClick) } + .clip(RoundedCornerShape(50)) + .clickable { onAction(DetailsAction.OpenRepoInBrowser) } .padding(horizontal = 12.dp, vertical = 10.dp), ) { Icon( - imageVector = Icons.Default.Share, - contentDescription = stringResource(Res.string.share_repository), + imageVector = Icons.Default.OpenInBrowser, + contentDescription = stringResource(Res.string.open_repository), tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.size(18.dp), ) @@ -882,6 +881,7 @@ private fun DetailsOverflowMenu( Box { Box( modifier = Modifier + .clip(RoundedCornerShape(50)) .clickable { menuOpen = true } .padding(horizontal = 12.dp, vertical = 10.dp), contentAlignment = Alignment.Center, @@ -945,17 +945,17 @@ private fun DetailsOverflowMenu( ) state.repository?.htmlUrl?.let { GhsDropdownMenuItem( - text = stringResource(Res.string.open_repository), + text = stringResource(Res.string.share_repository), leadingIcon = { Icon( - imageVector = Icons.Default.OpenInBrowser, + imageVector = Icons.Default.Share, contentDescription = null, modifier = Modifier.size(18.dp), ) }, onClick = { menuOpen = false - onAction(DetailsAction.OpenRepoInBrowser) + onAction(DetailsAction.OnShareClick) }, ) } From 38443472b64b4d777901ea9b8930e256a7a23f95 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:21:03 +0500 Subject: [PATCH 060/172] fix(details/header): auto-shrink long app names to fit --- .../details/presentation/components/AppHeader.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt index 2480fea19..015d7c995 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/AppHeader.kt @@ -231,16 +231,21 @@ fun AppHeader( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( + androidx.compose.foundation.text.BasicText( text = repository.name, style = MaterialTheme.typography.headlineMedium.copy( fontWeight = FontWeight.Black, fontSize = 30.sp, letterSpacing = (-0.4).sp, + color = MaterialTheme.colorScheme.onSurface, + ), + maxLines = 1, + softWrap = false, + autoSize = androidx.compose.foundation.text.TextAutoSize.StepBased( + minFontSize = 18.sp, + maxFontSize = 30.sp, + stepSize = 1.sp, ), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) if (repository.isFork) ForkBadge() From d5a7dafe59c8bdb8ceb116cfd680b5f6df738076 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:21:04 +0500 Subject: [PATCH 061/172] feat(details/owner): show display name with @handle --- .../presentation/components/sections/Owner.kt | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt index 72273e266..38f07b176 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Owner.kt @@ -107,16 +107,38 @@ fun LazyListScope.author( ) Column( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(3.dp), ) { - author?.login?.let { - Text( + val displayName = author?.name?.takeIf { it.isNotBlank() } ?: author?.login + val handle = author?.login?.takeIf { it != author.name } + displayName?.let { + androidx.compose.foundation.text.BasicText( text = it, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 17.sp, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Black, + fontSize = 22.sp, + letterSpacing = (-0.3).sp, + color = MaterialTheme.colorScheme.onSurface, + ), + maxLines = 1, + softWrap = false, + autoSize = androidx.compose.foundation.text.TextAutoSize.StepBased( + minFontSize = 15.sp, + maxFontSize = 22.sp, + stepSize = 1.sp, + ), + ) + } + handle?.let { login -> + Text( + text = "@$login", + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, ), - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } author?.bio?.let { bio -> From ac465c3c4f65aae368febc9cd624703f8108e856 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:25:06 +0500 Subject: [PATCH 062/172] feat(details): redesign SmartInstallButton with proper ripple and inline progress --- .../components/SmartInstallButton.kt | 983 +++++++++--------- 1 file changed, 468 insertions(+), 515 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt index b8a887785..040b6a1a0 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt @@ -1,51 +1,56 @@ package zed.rainxch.details.presentation.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.filled.VerifiedUser +import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubAsset -import zed.rainxch.core.domain.model.GithubUser import zed.rainxch.core.domain.util.VersionMath import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.details.presentation.DetailsState @@ -72,7 +77,10 @@ import zed.rainxch.githubstore.core.presentation.res.updating import zed.rainxch.githubstore.core.presentation.res.verified_build import zed.rainxch.githubstore.core.presentation.res.verifying -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +private val ButtonHeight = 56.dp +private val PillCorner = 28.dp +private val InnerCorner = 8.dp + @Composable fun SmartInstallButton( isDownloading: Boolean, @@ -88,474 +96,440 @@ fun SmartInstallButton( val isUpdateAvailable = installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall - val normInstalled = - installedApp?.installedVersion?.trim()?.takeIf { it.isNotBlank() } - val normSelected = - state.selectedRelease?.tagName?.trim()?.takeIf { it.isNotBlank() } - - val displaySelected = - normSelected?.let { tag -> - VersionMath.normalizeVersion(tag).takeIf { it.isNotBlank() } ?: tag - } - + val normInstalled = installedApp?.installedVersion?.trim()?.takeIf { it.isNotBlank() } + val normSelected = state.selectedRelease?.tagName?.trim()?.takeIf { it.isNotBlank() } + val displaySelected = normSelected?.let { tag -> + VersionMath.normalizeVersion(tag).takeIf { it.isNotBlank() } ?: tag + } val isSameVersionInstalled = isInstalled && normInstalled != null && normSelected != null && VersionMath.isExactSameVersion(normInstalled, normSelected) - val enabled = - remember(primaryAsset, isDownloading, isInstalling) { - primaryAsset != null && !isDownloading && !isInstalling - } - + val enabled = remember(primaryAsset, isDownloading, isInstalling) { + primaryAsset != null && !isDownloading && !isInstalling + } val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE if (isSameVersionInstalled && !isActiveDownload) { Column(modifier = modifier) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - - ElevatedCard( - onClick = { onAction(DetailsAction.OnRequestUninstall) }, - modifier = - Modifier - .weight(1f) - .height(52.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - ), - shape = - RoundedCornerShape( - topStart = 24.dp, - bottomStart = 24.dp, - topEnd = 6.dp, - bottomEnd = 6.dp, - ), - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onErrorContainer, - ) - Text( - text = stringResource(Res.string.uninstall), - color = MaterialTheme.colorScheme.onErrorContainer, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - ) - } - } - } - - ElevatedCard( - modifier = - Modifier - .weight(1f) - .height(52.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - shape = - RoundedCornerShape( - topStart = 6.dp, - bottomStart = 6.dp, - topEnd = 24.dp, - bottomEnd = 24.dp, - ), - onClick = { - onAction(DetailsAction.OpenApp) - }, - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - Text( - text = stringResource(Res.string.open_app), - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - ) - } - } - } - } - + InstalledSplitRow( + onUninstall = { onAction(DetailsAction.OnRequestUninstall) }, + onOpenApp = { onAction(DetailsAction.OpenApp) }, + ) AttestationBadge(attestationStatus = state.attestationStatus) } return } - val buttonColor = - when { - !enabled && !isActiveDownload -> MaterialTheme.colorScheme.surfaceContainer - isUpdateAvailable -> MaterialTheme.colorScheme.tertiary - isInstalled -> MaterialTheme.colorScheme.secondary - else -> MaterialTheme.colorScheme.primary - } + val accent = when { + !enabled && !isActiveDownload -> MaterialTheme.colorScheme.surfaceContainerHighest + isUpdateAvailable -> MaterialTheme.colorScheme.tertiary + isInstalled -> MaterialTheme.colorScheme.secondary + else -> MaterialTheme.colorScheme.primary + } + val onAccent = when { + !enabled && !isActiveDownload -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f) + isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary + isInstalled -> MaterialTheme.colorScheme.onSecondary + else -> MaterialTheme.colorScheme.onPrimary + } - val buttonText = - when { - !enabled && primaryAsset == null -> { - stringResource(Res.string.not_available) - } + val buttonText = when { + !enabled && primaryAsset == null -> stringResource(Res.string.not_available) + state.isPendingInstallReady -> stringResource(Res.string.install_ready) + isUpdateAvailable -> stringResource( + Res.string.update_to_version, + installedApp.latestVersion.toString(), + ) + isInstalled && + normInstalled != null && + normSelected != null && + !VersionMath.isExactSameVersion(normInstalled, normSelected) -> { + stringResource(Res.string.install_version, displaySelected ?: normSelected) + } + normSelected != null && + state.allReleases.firstOrNull()?.tagName?.let { latestTag -> + !VersionMath.isExactSameVersion(latestTag, normSelected) + } == true -> { + stringResource(Res.string.install_version, displaySelected ?: normSelected) + } + else -> stringResource(Res.string.install_latest) + } - state.isPendingInstallReady -> { - stringResource(Res.string.install_ready) - } + val hasTrailing = isActiveDownload || state.isObtainiumEnabled - isUpdateAvailable -> { - stringResource( - Res.string.update_to_version, - installedApp.latestVersion.toString(), + Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + val primaryShape = if (hasTrailing) { + RoundedCornerShape( + topStart = PillCorner, + bottomStart = PillCorner, + topEnd = InnerCorner, + bottomEnd = InnerCorner, ) + } else { + RoundedCornerShape(PillCorner) } + PrimaryAction( + modifier = Modifier.weight(1f), + shape = primaryShape, + accent = accent, + onAccent = onAccent, + enabled = enabled, + isActiveDownload = isActiveDownload, + isUpdateAvailable = isUpdateAvailable, + isInstalled = isInstalled, + buttonText = buttonText, + primaryAsset = primaryAsset, + state = state, + progress = progress, + onClick = { + if (!state.isDownloading && state.downloadStage == DownloadStage.IDLE) { + if (isUpdateAvailable) { + onAction(DetailsAction.UpdateApp) + } else { + onAction(DetailsAction.InstallPrimary) + } + } + }, + ) - isInstalled && - normInstalled != null && - normSelected != null && - !VersionMath.isExactSameVersion(normInstalled, normSelected) -> { - stringResource(Res.string.install_version, displaySelected ?: normSelected) + if (isActiveDownload) { + TrailingActionPill( + container = MaterialTheme.colorScheme.errorContainer, + content = MaterialTheme.colorScheme.onErrorContainer, + icon = Icons.Default.Close, + contentDescription = stringResource(Res.string.cancel_download), + onClick = { onAction(DetailsAction.CancelCurrentDownload) }, + ) + } else if (state.isObtainiumEnabled) { + TrailingActionPill( + container = if (enabled) accent else MaterialTheme.colorScheme.surfaceContainerHighest, + content = onAccent, + icon = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(Res.string.show_install_options), + onClick = { onAction(DetailsAction.OnToggleInstallDropdown) }, + ) } + } - normSelected != null && - state.allReleases.firstOrNull()?.tagName?.let { latestTag -> - !VersionMath.isExactSameVersion(latestTag, normSelected) - } == true -> { - stringResource(Res.string.install_version, displaySelected ?: normSelected) - } + AttestationBadge(attestationStatus = state.attestationStatus) + } +} - else -> { - stringResource(Res.string.install_latest) - } +@Composable +private fun PrimaryAction( + modifier: Modifier, + shape: RoundedCornerShape, + accent: Color, + onAccent: Color, + enabled: Boolean, + isActiveDownload: Boolean, + isUpdateAvailable: Boolean, + isInstalled: Boolean, + buttonText: String, + primaryAsset: GithubAsset?, + state: DetailsState, + progress: Int?, + onClick: () -> Unit, +) { + BoxWithConstraints(modifier = modifier) { + val totalWidthPx = constraints.maxWidth.toFloat().coerceAtLeast(1f) + val pct = progress?.coerceIn(0, 100) ?: 0 + val targetFraction = if (isActiveDownload && state.downloadStage == DownloadStage.DOWNLOADING) { + pct / 100f + } else if (isActiveDownload) { + 1f + } else { + 0f } - - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - ElevatedCard( - modifier = - Modifier - .weight(1f) - .height(52.dp) - .background( - color = buttonColor, - shape = CircleShape, - ).clickable( - enabled = enabled, - onClick = { - if (!state.isDownloading && state.downloadStage == DownloadStage.IDLE) { - if (isUpdateAvailable) { - onAction(DetailsAction.UpdateApp) - } else { - onAction(DetailsAction.InstallPrimary) - } - } - }, - ), - colors = - CardDefaults.elevatedCardColors( - containerColor = buttonColor, - ), - shape = - if (state.isObtainiumEnabled || isActiveDownload) { - RoundedCornerShape( - topStart = 24.dp, - bottomStart = 24.dp, - topEnd = 6.dp, - bottomEnd = 6.dp, - ) - } else { - CircleShape - }, + val animatedFraction by animateFloatAsState( + targetValue = targetFraction, + animationSpec = tween(durationMillis = 350), + label = "install-progress", + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(ButtonHeight) + .clip(shape) + .background(accent.copy(alpha = if (isActiveDownload) 0.35f else 1f)) + .clickable(enabled = enabled, onClick = onClick), ) { + if (isActiveDownload) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(with(androidx.compose.ui.platform.LocalDensity.current) { + (animatedFraction * totalWidthPx).toDp() + }) + .background(accent), + ) + } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { if (isActiveDownload) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - when (state.downloadStage) { - DownloadStage.DOWNLOADING -> { - Text( - text = - if (isUpdateAvailable) { - stringResource(Res.string.updating) - } else { - stringResource( - Res.string.downloading, - ) - }, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold, - ) - - val progressText = - if (state.totalBytes != null && state.totalBytes > 0) { - "${formatFileSize(state.downloadedBytes)} / ${ - formatFileSize( - state.totalBytes, - ) - }" - } else { - "${progress ?: 0}%" - } - Text( - text = progressText, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f), - ) - } - - DownloadStage.VERIFYING -> { - Text( - text = stringResource(Res.string.verifying), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold, - ) - } - - DownloadStage.INSTALLING -> { - Text( - text = - if (isUpdateAvailable) { - stringResource(Res.string.updating) - } else { - stringResource( - Res.string.installing, - ) - }, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold, - ) - } - - DownloadStage.IDLE -> {} - } - } + DownloadingLabel( + state = state, + progress = progress, + contentColor = onAccent, + isUpdateAvailable = isUpdateAvailable, + ) } else { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - if (isUpdateAvailable) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onTertiary, - ) - } else if (isInstalled) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onSecondary, - ) - } - - Text( - text = buttonText, - color = - if (enabled) { - when { - isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary - isInstalled -> MaterialTheme.colorScheme.onSecondary - else -> MaterialTheme.colorScheme.onPrimary - } - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) - }, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - if (primaryAsset != null) { - val assetArch = extractArchitectureFromName(primaryAsset.name) - val systemArch = state.systemArchitecture - val sizeText = formatFileSize(primaryAsset.size) - val archLabel = assetArch ?: systemArch.name.lowercase() - val subtitle = "$archLabel \u2022 $sizeText" - - Spacer(modifier = Modifier.height(2.dp)) - - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = subtitle, - color = - if (enabled) { - when { - isUpdateAvailable -> { - MaterialTheme.colorScheme.onTertiary.copy( - alpha = 0.8f, - ) - } - - isInstalled -> { - MaterialTheme.colorScheme.onSecondary.copy( - alpha = 0.8f, - ) - } - - else -> { - MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f) - } - } - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - }, - style = MaterialTheme.typography.bodySmall, - ) - - if (assetArch != null && - isExactArchitectureMatch( - assetName = primaryAsset.name.lowercase(), - systemArch = systemArch, - ) - ) { - Spacer(modifier = Modifier.width(4.dp)) - - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = stringResource(Res.string.architecture_compatible), - tint = - if (enabled) { - when { - isUpdateAvailable -> { - MaterialTheme.colorScheme.onTertiary.copy( - alpha = 0.8f, - ) - } - - isInstalled -> { - MaterialTheme.colorScheme.onSecondary.copy( - alpha = 0.8f, - ) - } + IdleLabel( + text = buttonText, + enabled = enabled, + contentColor = onAccent, + isUpdateAvailable = isUpdateAvailable, + isInstalled = isInstalled, + primaryAsset = primaryAsset, + state = state, + ) + } + } + } + } +} - else -> { - MaterialTheme.colorScheme.onPrimary.copy( - alpha = 0.8f, - ) - } - } - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - }, - modifier = Modifier.size(14.dp), - ) - } - } - } - } +@Composable +private fun IdleLabel( + text: String, + enabled: Boolean, + contentColor: Color, + isUpdateAvailable: Boolean, + isInstalled: Boolean, + primaryAsset: GithubAsset?, + state: DetailsState, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + val leadingIcon = when { + isUpdateAvailable -> Icons.Default.Update + isInstalled -> Icons.Default.CheckCircle + enabled -> Icons.Default.Download + else -> null + } + if (leadingIcon != null) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = contentColor, + ) + } + Text( + text = text, + color = contentColor, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + if (primaryAsset != null) { + val assetArch = extractArchitectureFromName(primaryAsset.name) + val systemArch = state.systemArchitecture + val sizeText = formatFileSize(primaryAsset.size) + val archLabel = assetArch ?: systemArch.name.lowercase() + val subtitle = "$archLabel · $sizeText" + Spacer(modifier = Modifier.height(2.dp)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = subtitle, + color = contentColor.copy(alpha = 0.78f), + style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp), + ) + if (assetArch != null && isExactArchitectureMatch( + assetName = primaryAsset.name.lowercase(), + systemArch = systemArch, + ) + ) { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = stringResource(Res.string.architecture_compatible), + tint = contentColor.copy(alpha = 0.78f), + modifier = Modifier.size(12.dp), + ) } } } + } +} - if (isActiveDownload) { - IconButton( - onClick = { - onAction(DetailsAction.CancelCurrentDownload) - }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - ), - modifier = Modifier.size(52.dp), - shape = - RoundedCornerShape( - topStart = 6.dp, - bottomStart = 6.dp, - topEnd = 24.dp, - bottomEnd = 24.dp, - ), +@Composable +private fun DownloadingLabel( + state: DetailsState, + progress: Int?, + contentColor: Color, + isUpdateAvailable: Boolean, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + val label = when (state.downloadStage) { + DownloadStage.DOWNLOADING -> if (isUpdateAvailable) { + stringResource(Res.string.updating) + } else { + stringResource(Res.string.downloading) + } + DownloadStage.VERIFYING -> stringResource(Res.string.verifying) + DownloadStage.INSTALLING -> if (isUpdateAvailable) { + stringResource(Res.string.updating) + } else { + stringResource(Res.string.installing) + } + DownloadStage.IDLE -> "" + } + Text( + text = label, + style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp), + color = contentColor, + fontWeight = FontWeight.SemiBold, + ) + if (state.downloadStage == DownloadStage.DOWNLOADING) { + Spacer(Modifier.height(2.dp)) + val progressText = if (state.totalBytes != null && state.totalBytes > 0) { + "${formatFileSize(state.downloadedBytes)} / ${formatFileSize(state.totalBytes)}" + } else { + "${progress ?: 0}%" + } + Text( + text = progressText, + style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp), + color = contentColor.copy(alpha = 0.78f), + ) + } + } +} + +@Composable +private fun TrailingActionPill( + container: Color, + content: Color, + icon: androidx.compose.ui.graphics.vector.ImageVector, + contentDescription: String, + onClick: () -> Unit, +) { + val shape = RoundedCornerShape( + topStart = InnerCorner, + bottomStart = InnerCorner, + topEnd = PillCorner, + bottomEnd = PillCorner, + ) + Box( + modifier = Modifier + .size(width = ButtonHeight, height = ButtonHeight) + .clip(shape) + .background(container) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = content, + modifier = Modifier.size(22.dp), + ) + } +} + +@Composable +private fun InstalledSplitRow( + onUninstall: () -> Unit, + onOpenApp: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + val leftShape = RoundedCornerShape( + topStart = PillCorner, + bottomStart = PillCorner, + topEnd = InnerCorner, + bottomEnd = InnerCorner, + ) + val rightShape = RoundedCornerShape( + topStart = InnerCorner, + bottomStart = InnerCorner, + topEnd = PillCorner, + bottomEnd = PillCorner, + ) + Box( + modifier = Modifier + .weight(1f) + .height(ButtonHeight) + .clip(leftShape) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.55f), + shape = leftShape, + ) + .clickable(onClick = onUninstall), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(Res.string.cancel_download), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onErrorContainer, + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = stringResource(Res.string.uninstall), + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp), ) } - } else if (state.isObtainiumEnabled) { - IconButton( - onClick = { - onAction(DetailsAction.OnToggleInstallDropdown) - }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = - if (enabled) { - buttonColor - } else { - MaterialTheme.colorScheme.surfaceContainer - }, - ), - modifier = Modifier.size(52.dp), - shape = - RoundedCornerShape( - topStart = 6.dp, - bottomStart = 6.dp, - topEnd = 24.dp, - bottomEnd = 24.dp, - ), + } + Box( + modifier = Modifier + .weight(1f) + .height(ButtonHeight) + .clip(rightShape) + .background(MaterialTheme.colorScheme.primary) + .clickable(onClick = onOpenApp), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = stringResource(Res.string.show_install_options), - modifier = Modifier.size(24.dp), - tint = - if (enabled) { - when { - isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary - isInstalled -> MaterialTheme.colorScheme.onSecondary - else -> MaterialTheme.colorScheme.onPrimary - } - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) - }, + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onPrimary, + ) + Text( + text = stringResource(Res.string.open_app), + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp), ) } } @@ -565,70 +539,81 @@ fun SmartInstallButton( @Composable private fun AttestationBadge(attestationStatus: AttestationStatus) { AnimatedVisibility( - visible = - attestationStatus == AttestationStatus.VERIFIED || - attestationStatus == AttestationStatus.CHECKING || - attestationStatus == AttestationStatus.UNABLE_TO_VERIFY, + visible = attestationStatus == AttestationStatus.VERIFIED || + attestationStatus == AttestationStatus.CHECKING || + attestationStatus == AttestationStatus.UNABLE_TO_VERIFY, enter = fadeIn(), exit = fadeOut(), ) { - Row( - modifier = Modifier.padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, + val (container, content, icon, label) = when (attestationStatus) { + AttestationStatus.VERIFIED -> AttestationVisual( + container = MaterialTheme.colorScheme.tertiaryContainer, + content = MaterialTheme.colorScheme.onTertiaryContainer, + icon = Icons.Filled.VerifiedUser, + label = stringResource(Res.string.verified_build), + ) + AttestationStatus.UNABLE_TO_VERIFY -> AttestationVisual( + container = MaterialTheme.colorScheme.surfaceContainerHigh, + content = MaterialTheme.colorScheme.onSurfaceVariant, + icon = Icons.Outlined.Warning, + label = stringResource(Res.string.unable_to_verify_attestation), + ) + AttestationStatus.CHECKING -> AttestationVisual( + container = MaterialTheme.colorScheme.surfaceContainerHigh, + content = MaterialTheme.colorScheme.onSurfaceVariant, + icon = null, + label = stringResource(Res.string.checking_attestation), + ) + else -> AttestationVisual( + container = Color.Transparent, + content = Color.Transparent, + icon = null, + label = "", + ) + } + Surface( + modifier = Modifier.padding(top = 10.dp), + shape = RoundedCornerShape(50), + color = container, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), ) { - when (attestationStatus) { - AttestationStatus.CHECKING -> { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (attestationStatus == AttestationStatus.CHECKING) { CircularProgressIndicator( - modifier = Modifier.size(14.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(13.dp), + strokeWidth = 1.6.dp, + color = content, ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = stringResource(Res.string.checking_attestation), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - AttestationStatus.VERIFIED -> { + } else if (icon != null) { Icon( - imageVector = Icons.Filled.VerifiedUser, + imageVector = icon, contentDescription = null, modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.tertiary, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.verified_build), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.tertiary, - fontWeight = FontWeight.SemiBold, + tint = content, ) } - - AttestationStatus.UNABLE_TO_VERIFY -> { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.unable_to_verify_attestation), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - else -> {} + Text( + text = label, + style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp), + color = content, + fontWeight = FontWeight.SemiBold, + ) } } } } +private data class AttestationVisual( + val container: Color, + val content: Color, + val icon: androidx.compose.ui.graphics.vector.ImageVector?, + val label: String, +) + private fun formatFileSize(bytes: Long): String = when { bytes >= 1_073_741_824 -> "%.1f GB".format(bytes / 1_073_741_824.0) @@ -636,35 +621,3 @@ private fun formatFileSize(bytes: Long): String = bytes >= 1_024 -> "%.1f KB".format(bytes / 1_024.0) else -> "$bytes B" } - -@Preview -@Composable -fun SmartInstallButtonDownloadingPreview() { - SmartInstallButton( - isDownloading = true, - isInstalling = false, - progress = 45, - primaryAsset = - GithubAsset( - id = 1L, - name = "app-arm64-v8a.apk", - contentType = "application/vnd.android.package-archive", - size = 50_000_000L, - downloadUrl = "https://example.com/app.apk", - uploader = - GithubUser( - id = 1L, - login = "developer", - avatarUrl = "", - htmlUrl = "", - ), - ), - onAction = {}, - state = - DetailsState( - isDownloading = true, - downloadStage = DownloadStage.DOWNLOADING, - downloadProgressPercent = 45, - ), - ) -} From a21696750ac1af48ec96d99cd93c71344a499e65 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:31:12 +0500 Subject: [PATCH 063/172] chore(details): strip dead translation params from sections --- .../details/presentation/DetailsRoot.kt | 72 ------------------- .../presentation/components/sections/About.kt | 13 +--- .../components/sections/WhatsNew.kt | 15 +--- 3 files changed, 2 insertions(+), 98 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index 7f314356f..b7ffbb8c9 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -101,7 +101,6 @@ import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.core.presentation.utils.isPullToRefreshSupported import zed.rainxch.core.domain.model.RefreshError import zed.rainxch.details.presentation.components.ApkInspectSheet -import zed.rainxch.details.presentation.components.LanguagePicker import zed.rainxch.details.presentation.components.sections.about import zed.rainxch.details.presentation.components.sections.author import zed.rainxch.details.presentation.components.sections.header @@ -111,7 +110,6 @@ import zed.rainxch.details.presentation.components.sections.stats import zed.rainxch.details.presentation.components.sections.releaseChannel import zed.rainxch.details.presentation.components.sections.whatsNew import zed.rainxch.details.presentation.components.states.ErrorState -import zed.rainxch.details.presentation.model.TranslationTarget import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.add_to_favourites import zed.rainxch.githubstore.core.presentation.res.cancel @@ -521,36 +519,6 @@ fun DetailsScreen( containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> - LanguagePicker( - isVisible = state.isLanguagePickerVisible, - selectedLanguageCode = - when (state.languagePickerTarget) { - TranslationTarget.About -> state.aboutTranslation.targetLanguageCode - TranslationTarget.WhatsNew -> state.whatsNewTranslation.targetLanguageCode - null -> null - }, - deviceLanguageCode = state.deviceLanguageCode, - onLanguageSelected = { language -> - when (state.languagePickerTarget) { - TranslationTarget.About -> { - onAction(DetailsAction.TranslateAbout(language.code)) - } - - TranslationTarget.WhatsNew -> { - onAction( - DetailsAction.TranslateWhatsNew( - language.code, - ), - ) - } - - null -> {} - } - onAction(DetailsAction.DismissLanguagePicker) - }, - onDismiss = { onAction(DetailsAction.DismissLanguagePicker) }, - ) - if (state.isLoading) { Box( modifier = Modifier.fillMaxSize(), @@ -661,16 +629,6 @@ fun DetailsScreen( collapsedHeight = collapsedSectionHeight, measuredHeightPx = state.whatsNewMeasuredHeightPx, onMeasured = { onAction(DetailsAction.OnWhatsNewMeasured(it)) }, - translationState = state.whatsNewTranslation, - onTranslateClick = { - onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode)) - }, - onLanguagePickerClick = { - onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.WhatsNew)) - }, - onToggleTranslation = { - onAction(DetailsAction.ToggleWhatsNewTranslation) - }, onReadMore = onReadMoreWhatsNew, ) } @@ -684,16 +642,6 @@ fun DetailsScreen( collapsedHeight = collapsedSectionHeight, measuredHeightPx = state.aboutMeasuredHeightPx, onMeasured = { onAction(DetailsAction.OnAboutMeasured(it)) }, - translationState = state.aboutTranslation, - onTranslateClick = { - onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode)) - }, - onLanguagePickerClick = { - onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.About)) - }, - onToggleTranslation = { - onAction(DetailsAction.ToggleAboutTranslation) - }, onReadMore = onReadMoreAbout, ) } @@ -707,16 +655,6 @@ fun DetailsScreen( collapsedHeight = collapsedSectionHeight, measuredHeightPx = state.aboutMeasuredHeightPx, onMeasured = { onAction(DetailsAction.OnAboutMeasured(it)) }, - translationState = state.aboutTranslation, - onTranslateClick = { - onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode)) - }, - onLanguagePickerClick = { - onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.About)) - }, - onToggleTranslation = { - onAction(DetailsAction.ToggleAboutTranslation) - }, onReadMore = onReadMoreAbout, ) } @@ -729,16 +667,6 @@ fun DetailsScreen( collapsedHeight = collapsedSectionHeight, measuredHeightPx = state.whatsNewMeasuredHeightPx, onMeasured = { onAction(DetailsAction.OnWhatsNewMeasured(it)) }, - translationState = state.whatsNewTranslation, - onTranslateClick = { - onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode)) - }, - onLanguagePickerClick = { - onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.WhatsNew)) - }, - onToggleTranslation = { - onAction(DetailsAction.ToggleWhatsNewTranslation) - }, onReadMore = onReadMoreWhatsNew, ) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt index b66da43ca..f1932191b 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt @@ -57,9 +57,7 @@ import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.parser.MarkdownParser import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.util.applyThemeAwareImages -import zed.rainxch.details.presentation.components.TranslationControls import zed.rainxch.details.presentation.markdown.githubStoreMarkdownComponents -import zed.rainxch.details.presentation.model.TranslationState import zed.rainxch.details.presentation.utils.MarkdownImageTransformer import zed.rainxch.details.presentation.utils.rememberMarkdownColors import zed.rainxch.details.presentation.utils.rememberMarkdownTypography @@ -75,10 +73,6 @@ fun LazyListScope.about( collapsedHeight: Dp, measuredHeightPx: Float?, onMeasured: (Float) -> Unit, - translationState: TranslationState, - onTranslateClick: () -> Unit, - onLanguagePickerClick: () -> Unit, - onToggleTranslation: () -> Unit, onReadMore: (() -> Unit)? = null, ) { item { @@ -116,12 +110,7 @@ fun LazyListScope.about( } item(key = "about_markdown") { - val raw = - if (translationState.isShowingTranslation && translationState.translatedText != null) { - translationState.translatedText - } else { - readmeMarkdown - } + val raw = readmeMarkdown val isDark = androidx.compose.foundation.isSystemInDarkTheme() val probeClient = org.koin.compose.koinInject( qualifier = org.koin.core.qualifier.named("test"), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt index edcd8f47f..108269b8e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt @@ -57,8 +57,6 @@ import com.mikepenz.markdown.compose.Markdown import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubRelease -import zed.rainxch.details.presentation.components.TranslationControls -import zed.rainxch.details.presentation.model.TranslationState import zed.rainxch.details.presentation.utils.MarkdownImageTransformer import zed.rainxch.details.presentation.utils.rememberMarkdownColors import zed.rainxch.details.presentation.utils.rememberMarkdownTypography @@ -71,10 +69,6 @@ fun LazyListScope.whatsNew( collapsedHeight: Dp, measuredHeightPx: Float?, onMeasured: (Float) -> Unit, - translationState: TranslationState, - onTranslateClick: () -> Unit, - onLanguagePickerClick: () -> Unit, - onToggleTranslation: () -> Unit, onReadMore: (() -> Unit)? = null, ) { item { @@ -134,7 +128,6 @@ fun LazyListScope.whatsNew( Spacer(Modifier.height(12.dp)) ExpandableMarkdownContent( - translationState = translationState, release = release, collapsedHeight = collapsedHeight, isExpanded = isExpanded, @@ -147,7 +140,6 @@ fun LazyListScope.whatsNew( @Composable private fun ExpandableMarkdownContent( - translationState: TranslationState, release: GithubRelease, collapsedHeight: Dp, isExpanded: Boolean, @@ -155,12 +147,7 @@ private fun ExpandableMarkdownContent( measuredHeightPx: Float?, onMeasured: (Float) -> Unit, ) { - val raw = - if (translationState.isShowingTranslation && translationState.translatedText != null) { - translationState.translatedText - } else { - release.description ?: stringResource(Res.string.no_release_notes) - } + val raw = release.description ?: stringResource(Res.string.no_release_notes) val isDark = androidx.compose.foundation.isSystemInDarkTheme() var fullChunks by remember(raw, isDark) { mutableStateOf?>(null) } From 97146c40f52529de1763638a2b7dc7532f18c7c9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:32:39 +0500 Subject: [PATCH 064/172] chore(details): drop orphan TranslationControls, outlined LinkedRepoBanner --- .../components/LinkedRepoBanner.kt | 4 +- .../components/TranslationControls.kt | 288 ------------------ 2 files changed, 3 insertions(+), 289 deletions(-) delete mode 100644 feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt index bce608e6e..068ac5501 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.shape.RoundedCornerShape import zed.rainxch.core.presentation.theme.tokens.Radii import androidx.compose.material.icons.Icons @@ -39,7 +40,8 @@ fun LinkedRepoBanner( Surface( modifier = modifier, shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerHighest, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), ) { Row( modifier = Modifier.padding(start = 12.dp, top = 8.dp, end = 4.dp, bottom = 8.dp), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt deleted file mode 100644 index a10384310..000000000 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt +++ /dev/null @@ -1,288 +0,0 @@ -package zed.rainxch.details.presentation.components - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.GTranslate -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.details.presentation.model.TranslationState -import zed.rainxch.githubstore.core.presentation.res.* - -@Composable -fun TranslationControls( - translationState: TranslationState, - onTranslateClick: () -> Unit, - onLanguagePickerClick: () -> Unit, - onToggleTranslation: () -> Unit, - modifier: Modifier = Modifier, -) { - AnimatedContent( - targetState = translationState.controlState, - modifier = modifier, - transitionSpec = { - (fadeIn() + slideInHorizontally { it / 3 }) togetherWith - (fadeOut() + slideOutHorizontally { -it / 3 }) - }, - label = "translation_controls", - ) { state -> - when (state) { - TranslationControlState.IDLE -> { - IdleControls( - onTranslateClick = onTranslateClick, - onLanguagePickerClick = onLanguagePickerClick, - ) - } - - TranslationControlState.TRANSLATING -> { - TranslatingIndicator() - } - - TranslationControlState.SHOWING_TRANSLATION -> { - TranslatedControls( - displayName = translationState.targetLanguageDisplayName, - isShowingTranslation = true, - onToggle = onToggleTranslation, - onLanguagePickerClick = onLanguagePickerClick, - ) - } - - TranslationControlState.SHOWING_ORIGINAL -> { - TranslatedControls( - displayName = translationState.targetLanguageDisplayName, - isShowingTranslation = false, - onToggle = onToggleTranslation, - onLanguagePickerClick = onLanguagePickerClick, - ) - } - - TranslationControlState.ERROR -> { - ErrorControls( - onRetry = onTranslateClick, - onLanguagePickerClick = onLanguagePickerClick, - ) - } - } - } -} - -@Composable -private fun IdleControls( - onTranslateClick: () -> Unit, - onLanguagePickerClick: () -> Unit, -) { - Row( - modifier = - Modifier - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .clickable(onClick = onTranslateClick) - .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Default.GTranslate, - contentDescription = stringResource(Res.string.translate), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp), - ) - Spacer(Modifier.width(5.dp)) - Text( - text = stringResource(Res.string.translate), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - LanguageDropdownButton(onLanguagePickerClick) - } -} - -@Composable -private fun TranslatingIndicator() { - Row( - modifier = - Modifier - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) - .padding(horizontal = 12.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - CircularProgressIndicator( - modifier = Modifier.size(14.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - Text( - text = stringResource(Res.string.translating), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } -} - -@Composable -private fun TranslatedControls( - displayName: String?, - isShowingTranslation: Boolean, - onToggle: () -> Unit, - onLanguagePickerClick: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - modifier = - Modifier - .clip(RoundedCornerShape(20.dp)) - .background( - if (isShowingTranslation) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHigh - }, - ).clickable(onClick = onToggle) - .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - AnimatedVisibility( - visible = isShowingTranslation, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), - ) { - Row { - Icon( - imageVector = Icons.Default.GTranslate, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(14.dp), - ) - Spacer(Modifier.width(4.dp)) - } - } - - Text( - text = - if (isShowingTranslation) { - stringResource(Res.string.show_original) - } else { - displayName ?: stringResource(Res.string.translate) - }, - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, - color = - if (isShowingTranslation) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - - LanguageDropdownButton( - onClick = onLanguagePickerClick, - tint = - if (isShowingTranslation) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - } - } -} - -@Composable -private fun ErrorControls( - onRetry: () -> Unit, - onLanguagePickerClick: () -> Unit, -) { - Row( - modifier = - Modifier - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.errorContainer) - .clickable(onClick = onRetry) - .padding(start = 10.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = stringResource(Res.string.translation_error_retry), - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.size(14.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.translation_error_retry), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - ) - LanguageDropdownButton( - onClick = onLanguagePickerClick, - tint = MaterialTheme.colorScheme.onErrorContainer, - ) - } -} - -@Composable -private fun LanguageDropdownButton( - onClick: () -> Unit, - tint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurfaceVariant, -) { - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = stringResource(Res.string.change_language), - tint = tint, - modifier = - Modifier - .size(24.dp) - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = onClick) - .padding(2.dp), - ) -} - -private enum class TranslationControlState { - IDLE, - TRANSLATING, - SHOWING_TRANSLATION, - SHOWING_ORIGINAL, - ERROR, -} - -private val TranslationState.controlState: TranslationControlState - get() = - when { - isTranslating -> TranslationControlState.TRANSLATING - error != null && translatedText == null -> TranslationControlState.ERROR - isShowingTranslation && translatedText != null -> TranslationControlState.SHOWING_TRANSLATION - !isShowingTranslation && translatedText != null -> TranslationControlState.SHOWING_ORIGINAL - else -> TranslationControlState.IDLE - } From 10504d410b4b9e43f93df35f87aa238edeb40b74 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:33:32 +0500 Subject: [PATCH 065/172] fix(details/whatsnew): drop !! on translatedText --- .../details/presentation/whatsnew/WhatsNewRoot.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt index 201f84042..9a829edad 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/whatsnew/WhatsNewRoot.kt @@ -208,15 +208,11 @@ private fun WhatsNewScreen( } Spacer(Modifier.height(6.dp)) val isLatest = index == 0 - val useTranslated = isLatest && - state.translation.isShowingTranslation && - state.translation.translatedText != null - val body = if (useTranslated) { - state.translation.translatedText!! - } else { - release.description?.takeIf { it.isNotBlank() } - ?: stringResource(Res.string.no_release_notes) - } + val translated = state.translation.translatedText + ?.takeIf { isLatest && state.translation.isShowingTranslation } + val body = translated + ?: release.description?.takeIf { it.isNotBlank() } + ?: stringResource(Res.string.no_release_notes) Markdown( content = body, colors = colors, From affe7cfc892038fffd8028ff220015212d9573cf Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:38:04 +0500 Subject: [PATCH 066/172] feat(search): floating-pill topbar, outlined banners, drop scroll-collapse --- .../rainxch/search/presentation/SearchRoot.kt | 189 +++++++----------- 1 file changed, 75 insertions(+), 114 deletions(-) diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index f8fae9e9e..bed86ad1d 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -1,9 +1,6 @@ package zed.rainxch.search.presentation import androidx.compose.animation.AnimatedVisibility -import androidx.compose.runtime.snapshotFlow -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically @@ -196,26 +193,6 @@ fun SearchScreen( val listState = rememberLazyStaggeredGridState() val bottomNavHeight = LocalBottomNavigationHeight.current - var showTopbar by remember { mutableStateOf(true) } - LaunchedEffect(listState) { - var prevIndex = 0 - var prevOffset = 0 - snapshotFlow { - listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset - }.collect { (index, offset) -> - val scrolledDown = index > prevIndex || (index == prevIndex && offset > prevOffset + 4) - val scrolledUp = index < prevIndex || (index == prevIndex && offset < prevOffset - 4) - showTopbar = when { - index == 0 && offset == 0 -> true - scrolledDown -> false - scrolledUp -> true - else -> showTopbar - } - prevIndex = index - prevOffset = offset - } - } - val shouldLoadMore by remember { derivedStateOf { val layoutInfo = listState.layoutInfo @@ -301,17 +278,11 @@ fun SearchScreen( Scaffold( topBar = { - AnimatedVisibility( - visible = showTopbar, - enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(), - exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(), - ) { - SearchTopbar( - onAction = onAction, - state = state, - focusRequester = focusRequester, - ) - } + SearchTopbar( + onAction = onAction, + state = state, + focusRequester = focusRequester, + ) }, snackbarHost = { SnackbarHost( @@ -767,23 +738,18 @@ private fun ClipboardBanner( onOpenLink: (ParsedGithubLink) -> Unit, onDismiss: () -> Unit, ) { - Card( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - ), - shape = RoundedCornerShape(12.dp), + androidx.compose.material3.Surface( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + shape = zed.rainxch.core.presentation.theme.tokens.Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = androidx.compose.foundation.BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ), ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), - ) { + Column(modifier = Modifier.padding(12.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -792,47 +758,43 @@ private fun ClipboardBanner( Text( text = stringResource(Res.string.clipboard_link_detected), style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, ) - IconButton( onClick = onDismiss, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(28.dp).clip(CircleShape), ) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(Res.string.dismiss), modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } - Spacer(Modifier.height(4.dp)) - links.forEach { link -> Row( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .clickable { onOpenLink(link) } - .padding(vertical = 6.dp, horizontal = 4.dp), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { onOpenLink(link) } + .padding(vertical = 8.dp, horizontal = 6.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Icon( imageVector = Icons.Default.Link, contentDescription = null, modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer, + tint = MaterialTheme.colorScheme.primary, ) Text( text = "${link.owner}/${link.repo}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f), ) Icon( @@ -853,58 +815,55 @@ private fun DetectedLinksSection( onOpenLink: (ParsedGithubLink) -> Unit, ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), ) { Text( text = stringResource(Res.string.detected_links), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(bottom = 4.dp), + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 6.dp), ) - links.forEach { link -> - Card( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 2.dp), + androidx.compose.material3.Surface( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 3.dp), onClick = { onOpenLink(link) }, - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - ), - shape = RoundedCornerShape(8.dp), + shape = zed.rainxch.core.presentation.theme.tokens.Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = androidx.compose.foundation.BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ), ) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Icon( imageVector = Icons.Default.Link, contentDescription = null, modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, + tint = MaterialTheme.colorScheme.primary, ) Text( text = "${link.owner}/${link.repo}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f), ) Text( text = stringResource(Res.string.open_in_app), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.SemiBold, ) } } @@ -923,9 +882,9 @@ private fun SearchTopbar( Modifier .fillMaxWidth() .statusBarsPadding() - .padding(horizontal = 8.dp, vertical = 8.dp), + .padding(horizontal = 14.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { TextField( value = state.query, @@ -937,28 +896,30 @@ private fun SearchTopbar( imageVector = Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, - trailingIcon = { - IconButton( - onClick = { - onAction(SearchAction.OnClearClick) - }, - modifier = - Modifier - .size(24.dp) + trailingIcon = if (state.query.isNotEmpty()) { + { + IconButton( + onClick = { onAction(SearchAction.OnClearClick) }, + modifier = Modifier + .size(28.dp) .clip(CircleShape), - ) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = null, - ) + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = stringResource(Res.string.dismiss), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - }, + } else null, placeholder = { Text( text = stringResource(Res.string.search_repositories_hint), - style = MaterialTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, softWrap = false, maxLines = 1, @@ -966,7 +927,7 @@ private fun SearchTopbar( ) }, textStyle = - MaterialTheme.typography.bodyLarge.copy( + MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface, ), keyboardOptions = @@ -987,10 +948,10 @@ private fun SearchTopbar( TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, ), - shape = CircleShape, + shape = RoundedCornerShape(50), modifier = Modifier .weight(1f) From 9244075164f4161fcdc29031badea357999c2724 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:45:25 +0500 Subject: [PATCH 067/172] feat(search): consolidate filters into single sheet with active chips strip --- .../composeResources/values/strings.xml | 10 + .../search/presentation/SearchAction.kt | 2 + .../rainxch/search/presentation/SearchRoot.kt | 326 ++++++++++-------- .../search/presentation/SearchState.kt | 1 + .../search/presentation/SearchViewModel.kt | 6 + .../components/SearchFiltersSheet.kt | 299 ++++++++++++++++ 6 files changed, 497 insertions(+), 147 deletions(-) create mode 100644 feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 0ba7ee171..969fc5471 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1183,6 +1183,16 @@ Retry Detected source: %1$s Change language + Filters + Filter results + Source + Platform + Language + Sort by + Reset all + Done + Active + Remove filter APK Inspect diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt index ce52a0db3..4dbca53b8 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt @@ -65,6 +65,8 @@ sealed interface SearchAction { data object OnFabClick : SearchAction + data object OnToggleFiltersSheet : SearchAction + data object DismissClipboardBanner : SearchAction data class OnHistoryItemClick( diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index bed86ad1d..513db8686 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -5,6 +5,8 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -37,6 +39,7 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.KeyboardArrowDown @@ -99,6 +102,7 @@ import zed.rainxch.search.presentation.components.SortByBottomSheet import zed.rainxch.search.presentation.model.ParsedGithubLink import zed.rainxch.search.presentation.model.ProgrammingLanguageUi import zed.rainxch.search.presentation.model.SearchPlatformUi +import zed.rainxch.search.presentation.model.SearchSourceUi import zed.rainxch.search.presentation.model.SortByUi import zed.rainxch.search.presentation.utils.label @@ -153,6 +157,34 @@ fun SearchRoot( }, ) + if (state.isFiltersSheetVisible) { + zed.rainxch.search.presentation.components.SearchFiltersSheet( + selectedSource = state.selectedSource, + availableSources = state.availableSources, + selectedPlatform = state.selectedSearchPlatform, + selectedLanguage = state.selectedLanguage, + selectedSortBy = state.selectedSortBy, + onSourceSelected = { viewModel.onAction(SearchAction.OnSourceSelected(it)) }, + onPlatformSelected = { viewModel.onAction(SearchAction.OnPlatformTypeSelected(it)) }, + onOpenLanguagePicker = { + viewModel.onAction(SearchAction.OnToggleFiltersSheet) + viewModel.onAction(SearchAction.OnToggleLanguageSheetVisibility) + }, + onOpenSortPicker = { + viewModel.onAction(SearchAction.OnToggleFiltersSheet) + viewModel.onAction(SearchAction.OnToggleSortByDialogVisibility) + }, + onReset = { + viewModel.onAction(SearchAction.OnLanguageSelected(ProgrammingLanguageUi.All)) + viewModel.onAction(SearchAction.OnPlatformTypeSelected(SearchPlatformUi.All)) + viewModel.onAction(SearchAction.OnSortBySelected(SortByUi.BestMatch)) + }, + onDismiss = { + viewModel.onAction(SearchAction.OnToggleFiltersSheet) + }, + ) + } + if (state.isLanguageSheetVisible) { LanguageFilterBottomSheet( selectedLanguage = state.selectedLanguage, @@ -355,153 +387,7 @@ fun SearchScreen( ) } - LazyRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - items(state.availableSources) { source -> - FilterChip( - selected = state.selectedSource == source, - label = { - Text( - text = source.label, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - onAction(SearchAction.OnSourceSelected(source)) - }, - ) - } - } - - LazyRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - items(SearchPlatformUi.entries) { sortBy -> - FilterChip( - selected = state.selectedSearchPlatform == sortBy, - label = { - Text( - text = sortBy.name.lowercase().replaceFirstChar { it.uppercase() }, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - onAction(SearchAction.OnPlatformTypeSelected(sortBy)) - }, - ) - } - } - - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(Res.string.language_label), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Medium, - ) - - FilterChip( - selected = state.selectedLanguage != ProgrammingLanguageUi.All, - onClick = { - onAction(SearchAction.OnToggleLanguageSheetVisibility) - }, - label = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - text = stringResource(state.selectedLanguage.label()), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - ) - Icon( - imageVector = Icons.Outlined.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - }, - ) - - if (state.selectedLanguage != ProgrammingLanguageUi.All) { - IconButton( - onClick = { - onAction(SearchAction.OnLanguageSelected(ProgrammingLanguageUi.All)) - }, - modifier = Modifier.size(32.dp), - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(Res.string.sort_label), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.Medium, - ) - - FilterChip( - selected = state.selectedSortBy != SortByUi.BestMatch, - onClick = { - onAction(SearchAction.OnToggleSortByDialogVisibility) - }, - label = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text( - text = stringResource(state.selectedSortBy.label()), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - ) - Icon( - imageVector = Icons.Outlined.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - }, - ) - } - } + ActiveFiltersStrip(state = state, onAction = onAction) Spacer(Modifier.height(6.dp)) @@ -877,6 +763,7 @@ private fun SearchTopbar( state: SearchState, focusRequester: FocusRequester, ) { + val activeFilterCount = activeFilterCount(state) Row( modifier = Modifier @@ -957,6 +844,151 @@ private fun SearchTopbar( .weight(1f) .focusRequester(focusRequester), ) + + FiltersPillButton( + activeCount = activeFilterCount, + onClick = { onAction(SearchAction.OnToggleFiltersSheet) }, + ) + } +} + +private fun activeFilterCount(state: SearchState): Int { + var count = 0 + if (state.selectedSource != SearchSourceUi.GitHub) count++ + if (state.selectedSearchPlatform != SearchPlatformUi.All) count++ + if (state.selectedLanguage != ProgrammingLanguageUi.All) count++ + if (state.selectedSortBy != SortByUi.BestMatch) count++ + return count +} + +@Composable +private fun FiltersPillButton( + activeCount: Int, + onClick: () -> Unit, +) { + val shape = RoundedCornerShape(50) + val container = + if (activeCount > 0) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceContainerLow + val content = + if (activeCount > 0) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurface + Row( + modifier = Modifier + .height(48.dp) + .clip(shape) + .background(container, shape) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Default.FilterList, + contentDescription = stringResource(Res.string.search_filters_button), + modifier = Modifier.size(18.dp), + tint = content, + ) + if (activeCount > 0) { + Text( + text = activeCount.toString(), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + ) + } + } +} + +@Composable +private fun ActiveFiltersStrip( + state: SearchState, + onAction: (SearchAction) -> Unit, +) { + val items = buildList Unit, androidx.compose.ui.graphics.vector.ImageVector?>> { + if (state.selectedSource != SearchSourceUi.GitHub) { + add(Triple(state.selectedSource.label, { onAction(SearchAction.OnSourceSelected(SearchSourceUi.GitHub)) }, null)) + } + if (state.selectedSearchPlatform != SearchPlatformUi.All) { + add( + Triple( + state.selectedSearchPlatform.name.lowercase().replaceFirstChar { it.uppercase() }, + { onAction(SearchAction.OnPlatformTypeSelected(SearchPlatformUi.All)) }, + null, + ), + ) + } + if (state.selectedLanguage != ProgrammingLanguageUi.All) { + add( + Triple( + "${state.selectedLanguage}", + { onAction(SearchAction.OnLanguageSelected(ProgrammingLanguageUi.All)) }, + Icons.Outlined.KeyboardArrowDown, + ), + ) + } + if (state.selectedSortBy != SortByUi.BestMatch) { + add( + Triple( + "${state.selectedSortBy}", + { onAction(SearchAction.OnSortBySelected(SortByUi.BestMatch)) }, + Icons.AutoMirrored.Filled.Sort, + ), + ) + } + } + if (items.isEmpty()) return + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + items.forEach { (label, onRemove, leading) -> + ActiveFilterChip(label = label, leadingIcon = leading, onRemove = onRemove) + } + } +} + +@Composable +private fun ActiveFilterChip( + label: String, + leadingIcon: androidx.compose.ui.graphics.vector.ImageVector?, + onRemove: () -> Unit, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = Modifier + .clip(shape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), shape) + .border(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), shape) + .clickable(onClick = onRemove) + .padding(start = 12.dp, end = 8.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (leadingIcon != null) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.primary, + ) + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(Res.string.search_clear_filter_cd), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary, + ) } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt index a45607d49..998eec7ee 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt @@ -38,6 +38,7 @@ data class SearchState( val selectedSource: SearchSourceUi = SearchSourceUi.GitHub, val availableSources: ImmutableList = persistentListOf(SearchSourceUi.GitHub, SearchSourceUi.Codeberg), + val isFiltersSheetVisible: Boolean = false, ) { enum class ExploreStatus { IDLE, diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index eb15dd200..7806f0831 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -587,6 +587,12 @@ class SearchViewModel( } } + SearchAction.OnToggleFiltersSheet -> { + _state.update { + it.copy(isFiltersSheetVisible = !it.isFiltersSheetVisible) + } + } + SearchAction.OnSearchImeClick -> { if (_state.value.detectedLinks.isNotEmpty() && isEntirelyGithubUrls(_state.value.query)) { val link = _state.value.detectedLinks.first() diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt new file mode 100644 index 000000000..9d0f12eeb --- /dev/null +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt @@ -0,0 +1,299 @@ +@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) + +package zed.rainxch.search.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.collections.immutable.ImmutableList +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.search_filters_apply +import zed.rainxch.githubstore.core.presentation.res.search_filters_reset +import zed.rainxch.githubstore.core.presentation.res.search_filters_section_language +import zed.rainxch.githubstore.core.presentation.res.search_filters_section_platform +import zed.rainxch.githubstore.core.presentation.res.search_filters_section_sort +import zed.rainxch.githubstore.core.presentation.res.search_filters_section_source +import zed.rainxch.githubstore.core.presentation.res.search_filters_title +import zed.rainxch.search.presentation.model.ProgrammingLanguageUi +import zed.rainxch.search.presentation.model.SearchPlatformUi +import zed.rainxch.search.presentation.model.SearchSourceUi +import zed.rainxch.search.presentation.model.SortByUi +import zed.rainxch.search.presentation.utils.label + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SearchFiltersSheet( + selectedSource: SearchSourceUi, + availableSources: ImmutableList, + selectedPlatform: SearchPlatformUi, + selectedLanguage: ProgrammingLanguageUi, + selectedSortBy: SortByUi, + onSourceSelected: (SearchSourceUi) -> Unit, + onPlatformSelected: (SearchPlatformUi) -> Unit, + onOpenLanguagePicker: () -> Unit, + onOpenSortPicker: () -> Unit, + onReset: () -> Unit, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + GhsBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(Res.string.search_filters_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + TextButton(onClick = onReset) { + Text( + text = stringResource(Res.string.search_filters_reset), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.primary, + ) + } + } + + FilterSection(title = stringResource(Res.string.search_filters_section_source)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + availableSources.forEach { source -> + SelectableChip( + text = source.label, + selected = selectedSource == source, + onClick = { onSourceSelected(source) }, + ) + } + } + } + + FilterSection(title = stringResource(Res.string.search_filters_section_platform)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SearchPlatformUi.entries.forEach { platform -> + SelectableChip( + text = platform.name + .lowercase() + .replaceFirstChar { it.uppercase() }, + selected = selectedPlatform == platform, + onClick = { onPlatformSelected(platform) }, + ) + } + } + } + + FilterSection(title = stringResource(Res.string.search_filters_section_language)) { + NavRow( + leadingIcon = Icons.Outlined.Language, + value = stringResource(selectedLanguage.label()), + onClick = onOpenLanguagePicker, + ) + } + + FilterSection(title = stringResource(Res.string.search_filters_section_sort)) { + NavRow( + leadingIcon = Icons.AutoMirrored.Filled.Sort, + value = stringResource(selectedSortBy.label()), + onClick = onOpenSortPicker, + ) + } + + Spacer(Modifier.height(4.dp)) + + Button( + onClick = onDismiss, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text( + text = stringResource(Res.string.search_filters_apply), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + } + Spacer(Modifier.height(8.dp)) + } + } +} + +@Composable +private fun FilterSection( + title: String, + content: @Composable () -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + content() + } +} + +@Composable +private fun SelectableChip( + text: String, + selected: Boolean, + onClick: () -> Unit, +) { + val container = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + val content = if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + } + val border = if (selected) { + BorderStroke(0.dp, MaterialTheme.colorScheme.primary) + } else { + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + } + Box( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .border(border, RoundedCornerShape(50)) + .background(container) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 9.dp), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (selected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = content, + ) + } + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium, + ), + color = content, + ) + } + } +} + +@Composable +private fun NavRow( + leadingIcon: androidx.compose.ui.graphics.vector.ImageVector, + value: String, + onClick: () -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + shape = RoundedCornerShape(14.dp), + ) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 14.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(12.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + } +} From 4814ce9a1d825578a6b0b5550dd7db6f7dbcfac5 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 16:50:38 +0500 Subject: [PATCH 068/172] feat(search): compact RepositoryCard + readable result count --- .../presentation/components/RepositoryCard.kt | 363 +++++++----------- .../rainxch/search/presentation/SearchRoot.kt | 6 +- 2 files changed, 144 insertions(+), 225 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 6e5a95ee3..676d228df 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -1,6 +1,7 @@ package zed.rainxch.core.presentation.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -88,6 +89,7 @@ import zed.rainxch.githubstore.core.presentation.res.update_available ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class, + androidx.compose.foundation.ExperimentalFoundationApi::class, ) @Composable fun RepositoryCard( @@ -100,7 +102,6 @@ fun RepositoryCard( onToggleSeen: (() -> Unit)? = null, ) { val uriHandler = LocalUriHandler.current - val contentAlpha by animateFloatAsState( targetValue = if (discoveryRepositoryUi.isSeen) 0.55f else 1f, animationSpec = tween(durationMillis = 300), @@ -109,260 +110,176 @@ fun RepositoryCard( var showActionsSheet by remember { mutableStateOf(false) } val sheetEnabled = onHideClick != null + val repo = discoveryRepositoryUi.repository - ExpressiveCard( - onClick = onClick, - onLongClick = if (sheetEnabled) { - { showActionsSheet = true } - } else { - null - }, - modifier = modifier, + Surface( + modifier = modifier + .fillMaxWidth() + .alpha(contentAlpha), + shape = zed.rainxch.core.presentation.theme.tokens.Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = androidx.compose.foundation.BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), ) { - Box(modifier = Modifier.alpha(contentAlpha)) { - if (discoveryRepositoryUi.isFavourite) { - Icon( - imageVector = Icons.Default.Favorite, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.08f), - modifier = - Modifier - .size(120.dp) - .align(Alignment.BottomStart) - .offset(x = (-32).dp, y = 32.dp), + Column( + modifier = Modifier + .combinedClickable( + onClick = onClick, + onLongClick = if (sheetEnabled) { + { showActionsSheet = true } + } else null, ) - } - - if (discoveryRepositoryUi.isStarred) { - Icon( - imageVector = Icons.Default.Star, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f), - modifier = - Modifier - .size(120.dp) - .align(Alignment.TopEnd) - .offset(x = 32.dp, y = (-32).dp), - ) - } - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + .padding(14.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { + GitHubStoreImage( + imageModel = { repo.owner.avatarUrl }, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .clickable { onDeveloperClick(repo.owner.login) }, + extractDominantFor = repo.owner.avatarUrl, + ) + Column(modifier = Modifier.weight(1f)) { Row( - modifier = - Modifier - .clip(CircleShape) - .clickable(onClick = { - onDeveloperClick(discoveryRepositoryUi.repository.owner.login) - }) - .padding(horizontal = 4.dp, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - GitHubStoreImage( - imageModel = { discoveryRepositoryUi.repository.owner.avatarUrl }, - modifier = - Modifier - .size(40.dp) - .clip(CircleShape), - ) - Text( - text = discoveryRepositoryUi.repository.owner.login, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, + text = repo.name, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - softWrap = false, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) - - if (discoveryRepositoryUi.isCurrentUserOwner) { - OfficialBadge() - } + if (discoveryRepositoryUi.isCurrentUserOwner) OfficialBadge() + if (repo.isFork) ForkBadge() } - - Text( - text = "/", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, - softWrap = false, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - modifier = Modifier.weight(1f), - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = discoveryRepositoryUi.repository.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) - - if (discoveryRepositoryUi.repository.isFork) { - ForkBadge() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = repo.owner.login, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .clip(CircleShape) + .clickable { onDeveloperClick(repo.owner.login) } + .padding(vertical = 2.dp, horizontal = 2.dp) + .weight(1f, fill = false), + ) + Text( + text = " · ${formatReleasedAt(repo.updatedAt)}", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } + } - Spacer(modifier = Modifier.height(4.dp)) - - discoveryRepositoryUi.repository.description?.let { - Text( - text = it, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyLarge, - softWrap = true, - ) - } + repo.description?.takeIf { it.isNotBlank() }?.let { desc -> + Spacer(Modifier.height(10.dp)) + Text( + text = desc, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall, + ) + } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - InfoChip( - icon = Icons.Outlined.StarOutline, - text = formatCount(discoveryRepositoryUi.repository.stargazersCount.toLong()), + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + if (repo.stargazersCount > 0) { + zed.rainxch.core.presentation.components.chips.StatChip( + label = formatCount(repo.stargazersCount.toLong()), + leading = { + Icon( + imageVector = Icons.Outlined.StarOutline, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, ) - - InfoChip( - icon = Icons.AutoMirrored.Outlined.CallSplit, - text = formatCount(discoveryRepositoryUi.repository.forksCount.toLong()), + } + if (repo.forksCount > 0) { + zed.rainxch.core.presentation.components.chips.StatChip( + label = formatCount(repo.forksCount.toLong()), + leading = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.CallSplit, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, ) - - if (discoveryRepositoryUi.repository.downloadCount > 0) { - InfoChip( - icon = Icons.Outlined.Download, - text = formatCount(discoveryRepositoryUi.repository.downloadCount), - ) - } - - discoveryRepositoryUi.repository.language?.let { - InfoChip( - icon = Icons.Outlined.Code, - text = it, - ) - } } - - if (discoveryRepositoryUi.isInstalled || discoveryRepositoryUi.isSeen) { - Spacer(Modifier.height(12.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (discoveryRepositoryUi.isInstalled) { - InstallStatusBadge( - isUpdateAvailable = discoveryRepositoryUi.isUpdateAvailable, + if (repo.downloadCount > 0) { + zed.rainxch.core.presentation.components.chips.StatChip( + label = formatCount(repo.downloadCount), + leading = { + Icon( + imageVector = Icons.Outlined.Download, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - } - - if (discoveryRepositoryUi.isSeen) { - SeenBadge() - } - } + }, + ) } - - if (discoveryRepositoryUi.repository.availablePlatforms.isNotEmpty()) { - Spacer(Modifier.height(12.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - discoveryRepositoryUi.repository.availablePlatforms.forEach { platform -> - PlatformChip(platform = platform) + val platformKinds = repo.availablePlatforms + .mapNotNull { platform -> + when (platform) { + DiscoveryPlatform.Android -> zed.rainxch.core.presentation.vocabulary.PlatformKind.ANDROID + DiscoveryPlatform.Windows -> zed.rainxch.core.presentation.vocabulary.PlatformKind.WINDOWS + DiscoveryPlatform.Macos -> zed.rainxch.core.presentation.vocabulary.PlatformKind.MACOS + DiscoveryPlatform.Linux -> zed.rainxch.core.presentation.vocabulary.PlatformKind.LINUX + else -> null } } + if (platformKinds.isNotEmpty()) { + zed.rainxch.core.presentation.components.chips.PlatformsChip( + platforms = platformKinds.let { + kotlinx.collections.immutable.persistentListOf() + .addAll(it) + }, + ) } + } - Spacer(Modifier.height(8.dp)) - - val releasedAtText = - buildAnnotatedString { - if (hasWeekNotPassed(discoveryRepositoryUi.repository.updatedAt)) { - append("🔥 ") - } - - append(formatReleasedAt(discoveryRepositoryUi.repository.updatedAt)) - } - - Text( - text = releasedAtText, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - ) - - Spacer(Modifier.height(12.dp)) - + if (discoveryRepositoryUi.isInstalled || discoveryRepositoryUi.isSeen) { + Spacer(Modifier.height(10.dp)) Row( - modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - GithubStoreButton( - text = stringResource(Res.string.home_view_details), - onClick = onClick, - modifier = Modifier.weight(1f), - ) - - IconButton( - onClick = onShareClick, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - shapes = IconButtonDefaults.shapes(), - ) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = stringResource(Res.string.share_repository), + if (discoveryRepositoryUi.isInstalled) { + InstallStatusBadge( + isUpdateAvailable = discoveryRepositoryUi.isUpdateAvailable, ) } - - IconButton( - onClick = { - uriHandler.openUri(discoveryRepositoryUi.repository.htmlUrl) - }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - shapes = IconButtonDefaults.shapes(), - ) { - Icon( - imageVector = Icons.Default.OpenInBrowser, - contentDescription = stringResource(Res.string.open_in_browser), - ) + if (discoveryRepositoryUi.isSeen) { + SeenBadge() } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 513db8686..3139b65f3 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -398,8 +398,10 @@ fun SearchScreen( Res.string.results_found, state.totalCount, ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier .fillMaxWidth() From 0289298c92cce70a5cc22ee50e0388d7ba788bef Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 17:31:35 +0500 Subject: [PATCH 069/172] feat(auth): outlined cards, pill buttons, mono device code, collapsed extras --- .../auth/presentation/AuthenticationRoot.kt | 880 +++++++++--------- 1 file changed, 457 insertions(+), 423 deletions(-) diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 171bf0856..2c07abdf3 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -1,3 +1,8 @@ +@file:OptIn( + androidx.compose.material3.ExperimentalMaterial3Api::class, + androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class, +) + package zed.rainxch.auth.presentation import androidx.compose.animation.AnimatedContent @@ -13,11 +18,14 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -33,29 +41,27 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.DoneAll -import androidx.compose.material.icons.filled.OpenWith +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.CardDefaults +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -66,6 +72,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight @@ -85,24 +92,13 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.auth.presentation.model.AuthLoginState import zed.rainxch.auth.presentation.model.GithubDeviceStartUi -import zed.rainxch.core.presentation.components.GithubStoreButton +import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.auth_use_device_code_instead -import zed.rainxch.githubstore.core.presentation.res.pat_cancel -import zed.rainxch.githubstore.core.presentation.res.pat_hide -import zed.rainxch.githubstore.core.presentation.res.pat_input_label -import zed.rainxch.githubstore.core.presentation.res.pat_input_placeholder -import zed.rainxch.githubstore.core.presentation.res.pat_open_settings -import zed.rainxch.githubstore.core.presentation.res.pat_sheet_description -import zed.rainxch.githubstore.core.presentation.res.pat_sheet_title -import zed.rainxch.githubstore.core.presentation.res.pat_show -import zed.rainxch.githubstore.core.presentation.res.pat_submit -import zed.rainxch.githubstore.core.presentation.res.pat_use_token_instead import zed.rainxch.githubstore.core.presentation.res.app_icon import zed.rainxch.githubstore.core.presentation.res.auth_check_status -import zed.rainxch.githubstore.core.presentation.res.auth_code_expires_in import zed.rainxch.githubstore.core.presentation.res.auth_error_with_message import zed.rainxch.githubstore.core.presentation.res.auth_polling_status import zed.rainxch.githubstore.core.presentation.res.auth_rate_limited @@ -113,6 +109,16 @@ import zed.rainxch.githubstore.core.presentation.res.ic_github import zed.rainxch.githubstore.core.presentation.res.more_requests import zed.rainxch.githubstore.core.presentation.res.more_requests_description import zed.rainxch.githubstore.core.presentation.res.open_github +import zed.rainxch.githubstore.core.presentation.res.pat_cancel +import zed.rainxch.githubstore.core.presentation.res.pat_hide +import zed.rainxch.githubstore.core.presentation.res.pat_input_label +import zed.rainxch.githubstore.core.presentation.res.pat_input_placeholder +import zed.rainxch.githubstore.core.presentation.res.pat_open_settings +import zed.rainxch.githubstore.core.presentation.res.pat_sheet_description +import zed.rainxch.githubstore.core.presentation.res.pat_sheet_title +import zed.rainxch.githubstore.core.presentation.res.pat_show +import zed.rainxch.githubstore.core.presentation.res.pat_submit +import zed.rainxch.githubstore.core.presentation.res.pat_use_token_instead import zed.rainxch.githubstore.core.presentation.res.redirecting_message import zed.rainxch.githubstore.core.presentation.res.sign_in_with_github import zed.rainxch.githubstore.core.presentation.res.signed_in @@ -133,9 +139,7 @@ fun AuthenticationRoot( ObserveAsEvents(viewModel.events) { event -> when (event) { - AuthenticationEvents.OnNavigateToMain -> { - onNavigateToHome() - } + AuthenticationEvents.OnNavigateToMain -> onNavigateToHome() } } @@ -145,7 +149,6 @@ fun AuthenticationRoot( ) } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun AuthenticationScreen( state: AuthenticationState, @@ -156,40 +159,37 @@ fun AuthenticationScreen( containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> Column( - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 24.dp), + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(Modifier.height(48.dp)) + Spacer(Modifier.height(56.dp)) val iconScale by animateFloatAsState( - targetValue = - when (state.loginState) { - is AuthLoginState.LoggedIn -> 0.9f - is AuthLoginState.Error -> 0.95f - else -> 1f - }, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), + targetValue = when (state.loginState) { + is AuthLoginState.LoggedIn -> 0.92f + is AuthLoginState.Error -> 0.96f + else -> 1f + }, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), label = "icon_scale", ) Image( painter = painterResource(Res.drawable.app_icon), contentDescription = null, - modifier = - Modifier - .size(120.dp) - .graphicsLayer { - scaleX = iconScale - scaleY = iconScale - }.clip(RoundedCornerShape(28.dp)), + modifier = Modifier + .size(96.dp) + .graphicsLayer { + scaleX = iconScale + scaleY = iconScale + } + .clip(RoundedCornerShape(24.dp)), contentScale = ContentScale.Crop, ) @@ -198,50 +198,32 @@ fun AuthenticationScreen( AnimatedContent( targetState = state.loginState, transitionSpec = { - val enter = - fadeIn(tween(350)) + - slideInVertically( - animationSpec = - spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessMediumLow, - ), - initialOffsetY = { it / 5 }, - ) + val enter = fadeIn(tween(350)) + slideInVertically( + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMediumLow, + ), + initialOffsetY = { it / 5 }, + ) val exit = fadeOut(tween(200)) enter togetherWith exit }, contentKey = { it::class }, - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), label = "auth_state", ) { authState -> when (authState) { - is AuthLoginState.LoggedOut -> { - StateLoggedOut(onAction = onAction) - } - - is AuthLoginState.DevicePrompt -> { - StateDevicePrompt( - state = state, - authState = authState, - onAction = onAction, - ) - } - - is AuthLoginState.Pending -> { - StatePending() - } - - is AuthLoginState.LoggedIn -> { - StateLoggedIn() - } - - is AuthLoginState.Error -> { - StateError( - authState = authState, - onAction = onAction, - ) - } + is AuthLoginState.LoggedOut -> StateLoggedOut(onAction = onAction) + is AuthLoginState.DevicePrompt -> StateDevicePrompt( + state = state, + authState = authState, + onAction = onAction, + ) + is AuthLoginState.Pending -> StatePending() + is AuthLoginState.LoggedIn -> StateLoggedIn() + is AuthLoginState.Error -> StateError(authState = authState, onAction = onAction) } } } @@ -259,113 +241,140 @@ fun AuthenticationScreen( @Composable private fun StateLoggedOut(onAction: (AuthenticationAction) -> Unit) { + var showMoreOptions by remember { mutableStateOf(false) } + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = stringResource(Res.string.unlock_full_experience), - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, ) - Spacer(Modifier.height(24.dp)) - - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(32.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - ) { - Row( - modifier = Modifier.padding(20.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.Top, - ) { - Box( - modifier = - Modifier - .size(48.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.OpenWith, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } - - Column( - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - text = stringResource(Res.string.more_requests), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - ) + Spacer(Modifier.height(20.dp)) - Text( - text = stringResource(Res.string.more_requests_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } + BenefitsCard() Spacer(Modifier.weight(1f)) - GithubStoreButton( + PrimaryPillButton( text = stringResource(Res.string.sign_in_with_github), - onClick = { onAction(AuthenticationAction.StartWebAuth) }, - icon = { + leadingIcon = { Icon( painter = painterResource(Res.drawable.ic_github), contentDescription = null, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimary, ) }, - modifier = Modifier.fillMaxWidth(), + onClick = { onAction(AuthenticationAction.StartWebAuth) }, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(10.dp)) - TextButton(onClick = { onAction(AuthenticationAction.OpenPatSheet) }) { + TextButton(onClick = { onAction(AuthenticationAction.SkipLogin) }) { Text( - text = stringResource(Res.string.pat_use_token_instead), + text = stringResource(Res.string.continue_as_guest), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - TextButton(onClick = { onAction(AuthenticationAction.StartLogin) }) { - Text( - text = stringResource(Res.string.auth_use_device_code_instead), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) + TextButton(onClick = { showMoreOptions = !showMoreOptions }) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = if (showMoreOptions) "Hide options" else "More sign-in options", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = null, + modifier = Modifier + .size(16.dp) + .graphicsLayer { rotationZ = if (showMoreOptions) 180f else 0f }, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - TextButton(onClick = { onAction(AuthenticationAction.SkipLogin) }) { - Text( - text = stringResource(Res.string.continue_as_guest), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, - ) + AnimatedVisibility(visible = showMoreOptions) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(Modifier.height(4.dp)) + TextButton(onClick = { onAction(AuthenticationAction.OpenPatSheet) }) { + Text( + text = stringResource(Res.string.pat_use_token_instead), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + TextButton(onClick = { onAction(AuthenticationAction.StartLogin) }) { + Text( + text = stringResource(Res.string.auth_use_device_code_instead), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } Spacer(Modifier.height(16.dp)) } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun BenefitsCard() { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), + ) { + Row( + modifier = Modifier.padding(18.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(Res.drawable.ic_github), + contentDescription = null, + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResource(Res.string.more_requests), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = stringResource(Res.string.more_requests_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + @Composable private fun StateDevicePrompt( state: AuthenticationState, @@ -378,26 +387,22 @@ private fun StateDevicePrompt( ) { Spacer(Modifier.weight(1f)) - ElevatedCard( + Surface( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(32.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), ) { Column( - modifier = Modifier.padding(24.dp), + modifier = Modifier.padding(22.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = stringResource(Res.string.enter_code_on_github), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - - Spacer(Modifier.height(16.dp)) - + Spacer(Modifier.height(14.dp)) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, @@ -405,123 +410,94 @@ private fun StateDevicePrompt( ) { Text( text = authState.start.userCode, - style = MaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineLarge.copy( + fontWeight = FontWeight.Bold, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + ), color = MaterialTheme.colorScheme.onSurface, - letterSpacing = 2.sp, + letterSpacing = 3.sp, ) - Spacer(Modifier.width(12.dp)) - - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - onAction(AuthenticationAction.CopyCode(authState.start)) - }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)) + .clickable { + onAction(AuthenticationAction.CopyCode(authState.start)) + }, + contentAlignment = Alignment.Center, ) { AnimatedContent( targetState = state.copied, transitionSpec = { - (scaleIn( - spring(dampingRatio = Spring.DampingRatioMediumBouncy), - ) + fadeIn()) togetherWith (scaleOut() + fadeOut()) + (scaleIn(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeIn()) togetherWith + (scaleOut() + fadeOut()) }, label = "copy_icon", ) { isCopied -> Icon( - imageVector = - if (isCopied) { - Icons.Default.DoneAll - } else { - Icons.Default.ContentCopy - }, + imageVector = if (isCopied) Icons.Default.DoneAll else Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy_code), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.primary, ) } } } - state.info?.let { info -> - Spacer(Modifier.height(12.dp)) - + Spacer(Modifier.height(10.dp)) Text( text = info, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center, ) } - if (authState.remainingSeconds > 0) { - Spacer(Modifier.height(20.dp)) - - val progress = - authState.remainingSeconds.toFloat() / - authState.start.expiresInSec.toFloat() - + Spacer(Modifier.height(16.dp)) + val progress = authState.remainingSeconds.toFloat() / + authState.start.expiresInSec.toFloat() val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = tween(900), label = "countdown_progress", ) - val isUrgent = authState.remainingSeconds < 60 - val progressColor by animateColorAsState( - targetValue = - if (isUrgent) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - }, + targetValue = if (isUrgent) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, animationSpec = tween(500), label = "progress_color", ) - val timerColor by animateColorAsState( - targetValue = - if (isUrgent) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.outline - }, + targetValue = if (isUrgent) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, animationSpec = tween(500), label = "timer_color", ) - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { LinearProgressIndicator( progress = { animatedProgress }, - modifier = - Modifier - .weight(1f) - .clip(RoundedCornerShape(4.dp)), + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(4.dp)), color = progressColor, - trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + trackColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) - Spacer(Modifier.width(12.dp)) - val minutes = authState.remainingSeconds / 60 val seconds = authState.remainingSeconds % 60 - val formatted = - remember(minutes, seconds) { - "%02d:%02d".format(minutes, seconds) - } - + val formatted = remember(minutes, seconds) { + "%02d:%02d".format(minutes, seconds) + } Text( text = formatted, - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.Medium, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + ), color = timerColor, ) } @@ -529,57 +505,50 @@ private fun StateDevicePrompt( } } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(18.dp)) - GithubStoreButton( + PrimaryPillButton( text = stringResource(Res.string.open_github), - onClick = { - onAction(AuthenticationAction.OpenGitHub(authState.start)) - }, - icon = { + leadingIcon = { Icon( painter = painterResource(Res.drawable.ic_github), contentDescription = null, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onPrimary, ) }, - modifier = Modifier.fillMaxWidth(), + onClick = { onAction(AuthenticationAction.OpenGitHub(authState.start)) }, ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) - FilledTonalButton( - onClick = { onAction(AuthenticationAction.PollNow) }, - enabled = !state.isPolling, - shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth(), - ) { - if (state.isPolling) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) + OutlinedPillButton( + text = if (state.isPolling) { + stringResource(Res.string.auth_polling_status) } else { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - - Spacer(Modifier.width(8.dp)) - - Text( - text = - if (state.isPolling) { - stringResource(Res.string.auth_polling_status) - } else { - stringResource(Res.string.auth_check_status) - }, - style = MaterialTheme.typography.labelLarge, - ) - } + stringResource(Res.string.auth_check_status) + }, + leadingIcon = if (state.isPolling) { + { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } else { + { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + enabled = !state.isPolling, + onClick = { onAction(AuthenticationAction.PollNow) }, + ) if (state.pollIntervalSec > 0) { Spacer(Modifier.height(8.dp)) @@ -594,7 +563,6 @@ private fun StateDevicePrompt( } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun StatePending() { Column( @@ -602,12 +570,8 @@ private fun StatePending() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - CircularWavyProgressIndicator( - modifier = Modifier.size(64.dp), - ) - - Spacer(Modifier.height(24.dp)) - + CircularWavyProgressIndicator(modifier = Modifier.size(56.dp)) + Spacer(Modifier.height(20.dp)) Text( text = stringResource(Res.string.waiting_for_authorization), style = MaterialTheme.typography.titleMedium, @@ -625,38 +589,36 @@ private fun StateLoggedIn() { verticalArrangement = Arrangement.Center, ) { var visible by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { visible = true } - AnimatedVisibility( visible = visible, - enter = - scaleIn( - spring(dampingRatio = Spring.DampingRatioMediumBouncy), - ) + fadeIn(), + enter = scaleIn(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeIn(), ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - modifier = Modifier.size(72.dp), - tint = MaterialTheme.colorScheme.primary, - ) + Box( + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } } - - Spacer(Modifier.height(20.dp)) - + Spacer(Modifier.height(18.dp)) Text( text = stringResource(Res.string.signed_in), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onBackground, ) - - Spacer(Modifier.height(8.dp)) - + Spacer(Modifier.height(6.dp)) Text( text = stringResource(Res.string.redirecting_message), - style = MaterialTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -672,75 +634,129 @@ private fun StateError( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.weight(1f)) - - ElevatedCard( + Surface( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(32.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - ), + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.errorContainer, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.4f)), ) { Column( - modifier = Modifier.padding(24.dp), + modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - imageVector = Icons.Default.Warning, + imageVector = Icons.Outlined.WarningAmber, contentDescription = null, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(34.dp), tint = MaterialTheme.colorScheme.onErrorContainer, ) - - Spacer(Modifier.height(16.dp)) - + Spacer(Modifier.height(12.dp)) Text( - text = - stringResource( - Res.string.auth_error_with_message, - authState.message, - ), - style = MaterialTheme.typography.titleMedium, + text = stringResource(Res.string.auth_error_with_message, authState.message), + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onErrorContainer, textAlign = TextAlign.Center, ) - authState.recoveryHint?.let { hint -> - Spacer(Modifier.height(8.dp)) - + Spacer(Modifier.height(6.dp)) Text( text = hint, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.85f), textAlign = TextAlign.Center, ) } } } - - Spacer(Modifier.height(24.dp)) - - GithubStoreButton( + Spacer(Modifier.height(20.dp)) + PrimaryPillButton( text = stringResource(Res.string.try_again), onClick = { onAction(AuthenticationAction.StartLogin) }, - modifier = Modifier.fillMaxWidth(), ) - - Spacer(Modifier.height(8.dp)) - + Spacer(Modifier.height(6.dp)) TextButton(onClick = { onAction(AuthenticationAction.SkipLogin) }) { Text( text = stringResource(Res.string.continue_as_guest), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Spacer(Modifier.weight(2f)) } } -@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PrimaryPillButton( + text: String, + onClick: () -> Unit, + leadingIcon: (@Composable () -> Unit)? = null, + enabled: Boolean = true, +) { + Button( + onClick = onClick, + enabled = enabled, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(50), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + contentPadding = PaddingValues(horizontal = 20.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + leadingIcon?.invoke() + Text( + text = text, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + } + } +} + +@Composable +private fun OutlinedPillButton( + text: String, + onClick: () -> Unit, + leadingIcon: (@Composable () -> Unit)? = null, + enabled: Boolean = true, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(50), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)), + onClick = onClick, + enabled = enabled, + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + leadingIcon?.invoke() + if (leadingIcon != null) Spacer(Modifier.width(8.dp)) + Text( + text = text, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + @Composable private fun PatSignInSheet( input: String, @@ -750,48 +766,40 @@ private fun PatSignInSheet( ) { val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, - confirmValueChange = { newValue -> !(isSubmitting && newValue == SheetValue.Hidden) }, ) var isMasked by remember { mutableStateOf(true) } - ModalBottomSheet( + GhsBottomSheet( onDismissRequest = { if (!isSubmitting) onAction(AuthenticationAction.DismissPatSheet) }, sheetState = sheetState, ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 24.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), ) { Text( text = stringResource(Res.string.pat_sheet_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, ) - Text( text = stringResource(Res.string.pat_sheet_description), - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - FilledTonalButton( + OutlinedPillButton( + text = stringResource(Res.string.pat_open_settings), onClick = { onAction(AuthenticationAction.OpenPatSettingsPage) }, - shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(Res.string.pat_open_settings), - style = MaterialTheme.typography.labelLarge, - ) - } + ) OutlinedTextField( value = input, @@ -799,25 +807,33 @@ private fun PatSignInSheet( label = { Text(stringResource(Res.string.pat_input_label)) }, placeholder = { Text(stringResource(Res.string.pat_input_placeholder)) }, singleLine = true, - visualTransformation = - if (isMasked) PasswordVisualTransformation() else VisualTransformation.None, - keyboardOptions = - KeyboardOptions( - keyboardType = KeyboardType.Password, - autoCorrectEnabled = false, - capitalization = KeyboardCapitalization.None, - ), + visualTransformation = if (isMasked) PasswordVisualTransformation() else VisualTransformation.None, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrectEnabled = false, + capitalization = KeyboardCapitalization.None, + ), isError = error != null, enabled = !isSubmitting, + shape = RoundedCornerShape(14.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + ), trailingIcon = { - IconButton(onClick = { isMasked = !isMasked }) { + IconButton( + onClick = { isMasked = !isMasked }, + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + ) { Icon( - imageVector = - if (isMasked) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = - stringResource( - if (isMasked) Res.string.pat_show else Res.string.pat_hide, - ), + imageVector = if (isMasked) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = stringResource( + if (isMasked) Res.string.pat_show else Res.string.pat_hide, + ), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), ) } }, @@ -827,42 +843,70 @@ private fun PatSignInSheet( if (error != null) { Text( text = error, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, ) } - Spacer(Modifier.height(4.dp)) - Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - TextButton( + Surface( + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(50), + color = Color.Transparent, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), onClick = { onAction(AuthenticationAction.DismissPatSheet) }, enabled = !isSubmitting, - modifier = Modifier.weight(1f), ) { - Text(stringResource(Res.string.pat_cancel)) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(Res.string.pat_cancel), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + } } - GithubStoreButton( - text = stringResource(Res.string.pat_submit), + Button( onClick = { onAction(AuthenticationAction.SubmitPat) }, - modifier = Modifier.weight(1f), - icon = + enabled = !isSubmitting && input.isNotBlank(), + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(50), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { if (isSubmitting) { - { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary, - ) - } - } else { - null - }, - ) + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + Text( + text = stringResource(Res.string.pat_submit), + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + } + } } } } @@ -873,14 +917,12 @@ private fun PatSignInSheet( private fun PreviewError() { GithubStoreTheme { AuthenticationScreen( - state = - AuthenticationState( - loginState = - AuthLoginState.Error( - message = "Network timeout", - recoveryHint = "Check your internet connection", - ), + state = AuthenticationState( + loginState = AuthLoginState.Error( + message = "Network timeout", + recoveryHint = "Check your internet connection", ), + ), onAction = {}, ) } @@ -891,10 +933,7 @@ private fun PreviewError() { private fun PreviewLoggedOut() { GithubStoreTheme { AuthenticationScreen( - state = - AuthenticationState( - loginState = AuthLoginState.LoggedOut, - ), + state = AuthenticationState(loginState = AuthLoginState.LoggedOut), onAction = {}, ) } @@ -905,20 +944,18 @@ private fun PreviewLoggedOut() { private fun PreviewDevicePrompt() { GithubStoreTheme { AuthenticationScreen( - state = - AuthenticationState( - loginState = - AuthLoginState.DevicePrompt( - GithubDeviceStartUi( - deviceCode = "", - userCode = "2102-UHHUF", - verificationUri = "", - expiresInSec = 900, - ), - remainingSeconds = 847, - ), - copied = true, + state = AuthenticationState( + loginState = AuthLoginState.DevicePrompt( + GithubDeviceStartUi( + deviceCode = "", + userCode = "2102-UHHUF", + verificationUri = "", + expiresInSec = 900, + ), + remainingSeconds = 847, ), + copied = true, + ), onAction = {}, ) } @@ -929,10 +966,7 @@ private fun PreviewDevicePrompt() { private fun PreviewLoggedIn() { GithubStoreTheme { AuthenticationScreen( - state = - AuthenticationState( - loginState = AuthLoginState.LoggedIn, - ), + state = AuthenticationState(loginState = AuthLoginState.LoggedIn), onAction = {}, ) } From 1738d400fa394198d2efb7c6200ec84500a2ae63 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 17:31:39 +0500 Subject: [PATCH 070/172] feat(tweaks): two-axis theme picker, outlined vocab sweep --- .../rainxch/tweaks/presentation/TweaksRoot.kt | 8 +- .../components/ClearDownloadsDialog.kt | 6 +- .../components/CustomForgesDialog.kt | 1 + .../presentation/components/SectionText.kt | 25 +- .../components/ToggleSettingCard.kt | 53 +- .../components/sections/Appearance.kt | 538 ++++++++++-------- .../hosttokens/HostTokensEntryCard.kt | 48 +- 7 files changed, 391 insertions(+), 288 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 4575620ee..3bf928265 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -297,9 +297,11 @@ private fun TopAppBar() { title = { Text( text = stringResource(Res.string.tweaks_title), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = androidx.compose.ui.unit.TextUnit(22f, androidx.compose.ui.unit.TextUnitType.Sp), + ), + color = MaterialTheme.colorScheme.onBackground, ) }, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt index a28f7b1cc..c14b150d5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt @@ -42,9 +42,9 @@ fun ClearDownloadsDialog( modifier = modifier .padding(16.dp) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(16.dp), + .clip(zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape.Dialog) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(20.dp), ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt index f3dd0d34d..92ceb8c73 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt @@ -35,6 +35,7 @@ fun CustomForgesDialog( ) { AlertDialog( onDismissRequest = { onAction(TweaksAction.OnDismissCustomForgesDialog) }, + shape = zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape.Dialog, title = { Text(stringResource(Res.string.custom_forges_dialog_title)) }, text = { Column { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt index dd7dc8fcf..7c495d956 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt @@ -1,5 +1,7 @@ package zed.rainxch.tweaks.presentation.components +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -7,14 +9,23 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.vocabulary.Squiggle @Composable fun SectionHeader(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp), - ) + Column( + modifier = Modifier.padding(start = 4.dp, top = 12.dp, bottom = 4.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + Squiggle() + } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ToggleSettingCard.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ToggleSettingCard.kt index d5a1d6ca5..014603412 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ToggleSettingCard.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ToggleSettingCard.kt @@ -1,5 +1,6 @@ package zed.rainxch.tweaks.presentation.components +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -20,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.theme.tokens.Radii @Composable fun ToggleSettingCard( @@ -29,43 +31,48 @@ fun ToggleSettingCard( checked: Boolean, onCheckedChange: (Boolean) -> Unit, ) { - ExpressiveCard { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + ) { Row( - modifier = - Modifier - .fillMaxWidth() - .toggleable( - value = checked, - onValueChange = onCheckedChange, - role = Role.Switch, - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(), - ).padding(16.dp), + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = checked, + onValueChange = onCheckedChange, + role = Role.Switch, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + ) + .padding(horizontal = 16.dp, vertical = 14.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column( - modifier = - Modifier - .weight(1f) - .padding(end = 16.dp), + modifier = Modifier + .weight(1f) + .padding(end = 16.dp), ) { Text( text = title, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, ) - - Spacer(Modifier.height(4.dp)) - + Spacer(Modifier.height(2.dp)) Text( text = description, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Switch( checked = checked, onCheckedChange = null, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index b4e50764f..7d1b5b253 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -1,107 +1,88 @@ package zed.rainxch.tweaks.presentation.components.sections +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Colorize +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.LightMode -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material.icons.outlined.SettingsBrightness import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.Platform -import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.theme.tokens.Tokens +import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor import zed.rainxch.core.presentation.utils.displayName -import zed.rainxch.core.presentation.utils.primaryColor +import zed.rainxch.core.presentation.utils.toTokenPalette import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState import zed.rainxch.tweaks.presentation.components.SectionHeader import zed.rainxch.tweaks.presentation.components.ToggleSettingCard -@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.appearanceSection( state: TweaksState, onAction: (TweaksAction) -> Unit, ) { item { - SectionHeader( - text = stringResource(Res.string.section_appearance), - ) - + SectionHeader(text = stringResource(Res.string.section_appearance)) Spacer(Modifier.height(8.dp)) - ThemeSelectionCard( + ThemePickerCard( isDarkTheme = state.isDarkTheme, - onDarkThemeChange = { isDarkTheme -> - onAction(TweaksAction.OnDarkThemeChange(isDarkTheme)) - }, + selectedPalette = state.selectedThemeColor, + amoledEnabled = state.isAmoledThemeEnabled, + onDarkThemeChange = { onAction(TweaksAction.OnDarkThemeChange(it)) }, + onPaletteSelected = { onAction(TweaksAction.OnThemeColorSelected(it)) }, + onAmoledToggled = { onAction(TweaksAction.OnAmoledThemeToggled(it)) }, ) Spacer(Modifier.height(12.dp)) - ThemeColorCard( - selectedThemeColor = state.selectedThemeColor, - onThemeColorSelected = { theme -> - onAction(TweaksAction.OnThemeColorSelected(theme)) - }, - ) - - Spacer(Modifier.height(16.dp)) - - if (state.isDarkTheme == true || (state.isDarkTheme == null && isSystemInDarkTheme())) { - ToggleSettingCard( - title = stringResource(Res.string.amoled_black_theme), - description = stringResource(Res.string.amoled_black_description), - checked = state.isAmoledThemeEnabled, - onCheckedChange = { enabled -> - onAction(TweaksAction.OnAmoledThemeToggled(enabled)) - }, - ) - - Spacer(Modifier.height(8.dp)) - } - ToggleSettingCard( title = stringResource(Res.string.system_font), description = stringResource(Res.string.system_font_description), @@ -109,297 +90,376 @@ fun LazyListScope.appearanceSection( onCheckedChange = { enabled -> onAction( TweaksAction.OnFontThemeSelected( - if (enabled) { - FontTheme.SYSTEM - } else { - FontTheme.CUSTOM - }, + if (enabled) FontTheme.SYSTEM else FontTheme.CUSTOM, ), ) }, ) - Spacer(Modifier.height(8.dp)) - if (getPlatform() != Platform.ANDROID) { + Spacer(Modifier.height(8.dp)) ToggleSettingCard( title = stringResource(Res.string.scrollbar_option_title), description = stringResource(Res.string.scrollbar_option_description), checked = state.isScrollbarEnabled, - onCheckedChange = { enabled -> - onAction(TweaksAction.OnScrollbarToggled(enabled)) - }, + onCheckedChange = { onAction(TweaksAction.OnScrollbarToggled(it)) }, ) - Spacer(Modifier.height(8.dp)) - ContentWidthCard( selected = state.contentWidth, - onSelected = { width -> onAction(TweaksAction.OnContentWidthSelected(width)) }, + onSelected = { onAction(TweaksAction.OnContentWidthSelected(it)) }, ) } } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable -private fun ThemeSelectionCard( +private fun ThemePickerCard( isDarkTheme: Boolean?, + selectedPalette: AppTheme, + amoledEnabled: Boolean, onDarkThemeChange: (Boolean?) -> Unit, + onPaletteSelected: (AppTheme) -> Unit, + onAmoledToggled: (Boolean) -> Unit, ) { - ExpressiveCard { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - ThemeModeOption( - icon = Icons.Default.LightMode, - label = stringResource(Res.string.theme_light), - isSelected = isDarkTheme != null && !isDarkTheme, - onClick = { onDarkThemeChange(false) }, - modifier = Modifier.weight(1f), - ) + val systemDark = isSystemInDarkTheme() + val resolvedDark = isDarkTheme ?: systemDark + val previewMode = when { + resolvedDark && amoledEnabled -> Tokens.Mode.AMOLED + resolvedDark -> Tokens.Mode.DARK + else -> Tokens.Mode.LIGHT + } - ThemeModeOption( - icon = Icons.Default.DarkMode, - label = stringResource(Res.string.theme_dark), - isSelected = isDarkTheme == true, - onClick = { onDarkThemeChange(true) }, - modifier = Modifier.weight(1f), + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(Res.string.theme_color), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, ) - ThemeModeOption( - icon = Icons.Default.Colorize, - label = stringResource(Res.string.theme_system), - isSelected = isDarkTheme == null, - onClick = { onDarkThemeChange(null) }, - modifier = Modifier.weight(1f), + Spacer(Modifier.height(12.dp)) + + // Mode segment + ModeSegment( + isDarkTheme = isDarkTheme, + onChange = onDarkThemeChange, ) + + Spacer(Modifier.height(16.dp)) + + // Palette grid + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + AppTheme.entries.forEach { palette -> + PaletteSwatch( + palette = palette, + mode = previewMode, + selected = palette == selectedPalette, + onClick = { onPaletteSelected(palette) }, + ) + } + } + + AnimatedVisibility( + visible = resolvedDark, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column { + Spacer(Modifier.height(14.dp)) + InlineToggleRow( + title = stringResource(Res.string.amoled_black_theme), + description = stringResource(Res.string.amoled_black_description), + checked = amoledEnabled, + onCheckedChange = onAmoledToggled, + ) + } + } } } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun ThemeModeOption( - icon: ImageVector, - label: String, - isSelected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, +private fun ModeSegment( + isDarkTheme: Boolean?, + onChange: (Boolean?) -> Unit, ) { - val scale by animateFloatAsState( - targetValue = if (isSelected) 1.05f else 1f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ) - - Column( - modifier = - modifier - .scale(scale) - .clip(RoundedCornerShape(24.dp)) - .background( - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - }, - ).clickable(onClick = onClick) - .padding(vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + SegmentItem( + icon = Icons.Default.LightMode, + label = stringResource(Res.string.theme_light), + selected = isDarkTheme == false, + onClick = { onChange(false) }, + modifier = Modifier.weight(1f), ) - - Text( - text = label, - style = MaterialTheme.typography.titleMedium, - color = - if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface - }, + SegmentItem( + icon = Icons.Default.DarkMode, + label = stringResource(Res.string.theme_dark), + selected = isDarkTheme == true, + onClick = { onChange(true) }, + modifier = Modifier.weight(1f), + ) + SegmentItem( + icon = Icons.Outlined.SettingsBrightness, + label = stringResource(Res.string.theme_system), + selected = isDarkTheme == null, + onClick = { onChange(null) }, + modifier = Modifier.weight(1f), ) } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun ThemeColorCard( - selectedThemeColor: AppTheme, - onThemeColorSelected: (AppTheme) -> Unit, +private fun SegmentItem( + icon: ImageVector, + label: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { - ExpressiveCard { - Column( - modifier = Modifier.padding(16.dp), + val container = + if (selected) MaterialTheme.colorScheme.primary + else Color.Transparent + val content = + if (selected) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + Box( + modifier = modifier + .height(40.dp) + .clip(RoundedCornerShape(10.dp)) + .background(container) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = content, + ) Text( - text = stringResource(Res.string.theme_color), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, ) - - Spacer(Modifier.height(12.dp)) - - LazyRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - items(AppTheme.entries) { theme -> - ThemeColorOption( - theme = theme, - isSelected = selectedThemeColor == theme, - onClick = { onThemeColorSelected(theme) }, - ) - } - } } } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun ThemeColorOption( - theme: AppTheme, - isSelected: Boolean, +private fun PaletteSwatch( + palette: AppTheme, + mode: Tokens.Mode, + selected: Boolean, onClick: () -> Unit, ) { + val scheme = colorSchemeFor(palette.toTokenPalette(), mode) val scale by animateFloatAsState( - targetValue = if (isSelected) 1.1f else 1f, - animationSpec = - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow, - ), + targetValue = if (selected) 1.0f else 0.98f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "swatch_scale", ) - Column( - modifier = Modifier.clickable(onClick = onClick), + modifier = Modifier + .width(74.dp) + .scale(scale) + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onClick), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { Box( - modifier = - Modifier - .size(56.dp) - .scale(scale) - .clip( - if (isSelected) { - MaterialShapes.Cookie9Sided.toShape() - } else { - CircleShape - }, - ).background( - color = theme.primaryColor, - ), + modifier = Modifier + .size(width = 74.dp, height = 56.dp) + .clip(RoundedCornerShape(14.dp)) + .background(scheme.background) + .border( + width = if (selected) 2.dp else 1.dp, + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f) + }, + shape = RoundedCornerShape(14.dp), + ), contentAlignment = Alignment.Center, ) { - androidx.compose.animation.AnimatedVisibility( - visible = isSelected, - enter = scaleIn(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeIn(), - exit = scaleOut() + fadeOut(), + // Mini palette preview: primary blob + secondary dot + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Icon( - imageVector = Icons.Default.Done, - contentDescription = - stringResource( - Res.string.selected_color, - theme.displayName, - ), - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colorScheme.onPrimary, + Box( + modifier = Modifier + .size(width = 22.dp, height = 22.dp) + .clip(RoundedCornerShape(7.dp)) + .background(scheme.primary), + ) + Box( + modifier = Modifier + .size(14.dp) + .clip(CircleShape) + .background(scheme.secondary), + ) + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(scheme.tertiary), ) } + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp) + .size(16.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(11.dp), + ) + } + } } - Text( - text = theme.displayName, - style = MaterialTheme.typography.labelLarge, - color = - if (isSelected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + text = palette.displayName, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + ), + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } +} + +@Composable +private fun InlineToggleRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { onCheckedChange(!checked) } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = description, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = checked, + onCheckedChange = null, ) } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ContentWidthCard( selected: ContentWidth, onSelected: (ContentWidth) -> Unit, ) { - ExpressiveCard { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = stringResource(Res.string.content_width_title), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, ) - Text( text = stringResource(Res.string.content_width_description), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(Modifier.height(12.dp)) - Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { ContentWidth.entries.forEach { width -> val isSelected = width == selected - Column( + val container = + if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceContainerHigh + val content = + if (isSelected) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurface + Box( modifier = Modifier .weight(1f) - .clip(RoundedCornerShape(24.dp)) - .background( - if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - }, - ) - .clickable { onSelected(width) } - .padding(vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, + .height(40.dp) + .clip(RoundedCornerShape(50)) + .background(container) + .clickable { onSelected(width) }, + contentAlignment = Alignment.Center, ) { Text( text = width.displayName, - style = MaterialTheme.typography.titleMedium, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface - }, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, ) } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEntryCard.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEntryCard.kt index 75453d03a..21174319f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEntryCard.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensEntryCard.kt @@ -1,24 +1,30 @@ package zed.rainxch.tweaks.presentation.hosttokens +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.VpnKey -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.host_tokens_entry_subtitle import zed.rainxch.githubstore.core.presentation.res.host_tokens_title @@ -27,27 +33,42 @@ import zed.rainxch.githubstore.core.presentation.res.host_tokens_title fun HostTokensEntryCard( onClick: () -> Unit, ) { - OutlinedCard( + Surface( onClick = onClick, - colors = CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(32.dp), modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), ) { Row( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Icon( - imageVector = Icons.Default.VpnKey, - contentDescription = null, - ) + Box( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(11.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.VpnKey, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } Column(modifier = Modifier.weight(1f)) { Text( text = stringResource(Res.string.host_tokens_title), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, ) Text( text = stringResource(Res.string.host_tokens_entry_subtitle), @@ -58,6 +79,7 @@ fun HostTokensEntryCard( Icon( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } From 4b06a4ebd27dd0db80332b9ed4bab926051f321b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 17:39:38 +0500 Subject: [PATCH 071/172] feat(profile): outlined vocab, wire Tweaks entry, dev-profile refresh --- .../app/navigation/AppNavigation.kt | 3 + .../composeResources/values/strings.xml | 1 + .../components/DeveloperRepoItem.kt | 397 ++++++++---------- .../components/ProfileInfoCard.kt | 249 ++++++----- .../presentation/components/StatsRow.kt | 46 +- .../profile/presentation/ProfileAction.kt | 2 + .../profile/presentation/ProfileRoot.kt | 12 +- .../profile/presentation/ProfileViewModel.kt | 4 + .../components/sections/Account.kt | 120 ++---- .../components/sections/AccountSection.kt | 239 +++++------ .../components/sections/Options.kt | 228 ++++------ 11 files changed, 617 insertions(+), 684 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index a81c89d56..c8814d63c 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -578,6 +578,9 @@ fun AppNavigation( announcementsViewModel.previewSampleAnnouncements() navController.navigate(GithubStoreGraph.AnnouncementsScreen) }, + onNavigateToTweaks = { + navController.navigate(GithubStoreGraph.TweaksScreen) + }, hasUnreadAnnouncements = announcementsUnreadCount > 0, ) } diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 969fc5471..cfc9eba7e 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1193,6 +1193,7 @@ Done Active Remove filter + App settings, theme, network, translation, advanced options. APK Inspect diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt index 6fa13a7b7..8f8098904 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt @@ -2,10 +2,13 @@ package zed.rainxch.devprofile.presentation.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,33 +17,33 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.CallSplit +import androidx.compose.material.icons.automirrored.outlined.CallSplit import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.FavoriteBorder -import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.Badge -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconToggleButton +import androidx.compose.material.icons.outlined.StarOutline +import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.chips.StatChip import zed.rainxch.core.presentation.theme.GithubStoreTheme +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.devprofile.domain.model.DeveloperRepository import zed.rainxch.githubstore.core.presentation.res.* @@ -48,7 +51,7 @@ import kotlin.time.Clock import kotlin.time.ExperimentalTime import kotlin.time.Instant -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun DeveloperRepoItem( repository: DeveloperRepository, @@ -56,16 +59,17 @@ fun DeveloperRepoItem( onToggleFavorite: () -> Unit, modifier: Modifier = Modifier, ) { - ExpressiveCard( - onClick = onItemClick, + Surface( modifier = modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + onClick = onItemClick, ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - ) { + Column(modifier = Modifier.padding(14.dp)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -74,238 +78,204 @@ fun DeveloperRepoItem( Text( text = repository.name, maxLines = 1, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface, ) - Text( - text = - stringResource( - resource = Res.string.updated_on_date, - formatRelativeDate(repository.updatedAt), - ).replaceFirstChar { it.uppercase() }, - style = MaterialTheme.typography.titleMedium, + text = stringResource( + resource = Res.string.updated_on_date, + formatRelativeDate(repository.updatedAt), + ).replaceFirstChar { it.uppercase() }, + style = MaterialTheme.typography.labelMedium, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, ) } - - Spacer(modifier = Modifier.width(8.dp)) - - FilledIconToggleButton( - checked = repository.isFavorite, - onCheckedChange = { onToggleFavorite() }, - modifier = Modifier.size(40.dp), - shape = MaterialShapes.Cookie6Sided.toShape(), - ) { - Icon( - imageVector = - if (repository.isFavorite) { - Icons.Filled.Favorite - } else { - Icons.Outlined.FavoriteBorder - }, - contentDescription = - if (repository.isFavorite) { - stringResource(Res.string.remove_from_favourites) - } else { - stringResource(Res.string.add_to_favourites) - }, - modifier = Modifier.size(20.dp), - ) - } + Spacer(Modifier.width(8.dp)) + FavoriteToggle( + isFavorite = repository.isFavorite, + onClick = onToggleFavorite, + ) } - repository.description?.let { description -> - Spacer(modifier = Modifier.height(8.dp)) - + repository.description?.takeIf { it.isNotBlank() }?.let { description -> + Spacer(Modifier.height(8.dp)) Text( text = description, maxLines = 2, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + overflow = TextOverflow.Ellipsis, ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { - RepoStat( - icon = Icons.Default.Star, - value = formatCount(repository.stargazersCount), - contentDescription = $$"$${repository.stargazersCount} $${stringResource(Res.string.stars)}", - ) - - RepoStat( - icon = Icons.AutoMirrored.Filled.CallSplit, - value = formatCount(repository.forksCount), - contentDescription = "${repository.forksCount} ${stringResource(Res.string.forks)}", - ) - + if (repository.stargazersCount > 0) { + StatChip( + label = formatCount(repository.stargazersCount), + leading = { + Icon( + imageVector = Icons.Outlined.StarOutline, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + if (repository.forksCount > 0) { + StatChip( + label = formatCount(repository.forksCount), + leading = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.CallSplit, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } if (repository.openIssuesCount > 0) { - RepoStat( - icon = Icons.Outlined.Warning, - value = formatCount(repository.openIssuesCount), - contentDescription = "${repository.openIssuesCount} ${stringResource(Res.string.issues)}", + StatChip( + label = formatCount(repository.openIssuesCount), + leading = { + Icon( + imageVector = Icons.Outlined.WarningAmber, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, ) } - - repository.language?.let { language -> - SuggestionChip( - onClick = {}, - label = { - Text( - text = language, - style = MaterialTheme.typography.labelSmall, + repository.language?.takeIf { it.isNotBlank() }?.let { language -> + StatChip( + label = language, + leading = { + Icon( + imageVector = Icons.Outlined.Code, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, - modifier = Modifier.height(32.dp), ) } } - val repoBadges = - buildList { + val showBadges = repository.hasInstallableAssets || repository.isInstalled + if (showBadges) { + Spacer(Modifier.height(10.dp)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { if (repository.hasInstallableAssets) { - add( - RepoBadge( - text = - repository.latestVersion - ?: stringResource(Res.string.has_release), - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), + TonalBadge( + text = repository.latestVersion + ?: stringResource(Res.string.has_release), + container = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), + content = MaterialTheme.colorScheme.primary, ) } if (repository.isInstalled) { - add( - RepoBadge( - text = stringResource(Res.string.installed), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ), + TonalBadge( + text = stringResource(Res.string.installed), + container = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.18f), + content = MaterialTheme.colorScheme.tertiary, ) } } - - if (repoBadges.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - repoBadges.forEach { badge -> - Badge( - containerColor = badge.containerColor, - ) { - Text( - text = badge.text, - style = MaterialTheme.typography.labelSmall, - color = badge.contentColor, - ) - } - } - } } } } } @Composable -private fun RepoStat( - icon: ImageVector, - value: String, - contentDescription: String, - modifier: Modifier = Modifier, +private fun FavoriteToggle( + isFavorite: Boolean, + onClick: () -> Unit, ) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, + val container = if (isFavorite) { + MaterialTheme.colorScheme.error.copy(alpha = 0.18f) + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + val tint = if (isFavorite) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(container) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, ) { Icon( - imageVector = icon, - contentDescription = contentDescription, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, + contentDescription = stringResource( + if (isFavorite) Res.string.remove_from_favourites + else Res.string.add_to_favourites, + ), + modifier = Modifier.size(18.dp), + tint = tint, ) + } +} +@Composable +private fun TonalBadge(text: String, container: Color, content: Color) { + Surface( + shape = RoundedCornerShape(50), + color = container, + ) { Text( - text = value, - maxLines = 1, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = text, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), ) } } -private data class RepoBadge( - val text: String, - val containerColor: Color, - val contentColor: Color, -) - @Composable private fun formatRelativeDate(dateString: String): String { - val instant = - try { - Instant.parse(dateString) - } catch (_: IllegalArgumentException) { - return dateString - } + val instant = try { + Instant.parse(dateString) + } catch (_: IllegalArgumentException) { + return dateString + } val now = Clock.System.now() val duration = now - instant - return when { - duration.inWholeDays > 365 -> { - stringResource( - Res.string.time_years_ago, - (duration.inWholeDays / 365).toInt(), - ) - } - - duration.inWholeDays > 30 -> { - stringResource( - Res.string.time_months_ago, - (duration.inWholeDays / 30).toInt(), - ) - } - - duration.inWholeDays > 0 -> { - stringResource( - Res.string.time_days_ago, - duration.inWholeDays.toInt(), - ) - } - - duration.inWholeHours > 0 -> { - stringResource( - Res.string.time_hours_ago, - duration.inWholeHours.toInt(), - ) - } - - duration.inWholeMinutes > 0 -> { - stringResource( - Res.string.time_minutes_ago, - duration.inWholeMinutes.toInt(), - ) - } - - else -> { - stringResource(Res.string.just_now) - } + duration.inWholeDays > 365 -> + stringResource(Res.string.time_years_ago, (duration.inWholeDays / 365).toInt()) + duration.inWholeDays > 30 -> + stringResource(Res.string.time_months_ago, (duration.inWholeDays / 30).toInt()) + duration.inWholeDays > 0 -> + stringResource(Res.string.time_days_ago, duration.inWholeDays.toInt()) + duration.inWholeHours > 0 -> + stringResource(Res.string.time_hours_ago, duration.inWholeHours.toInt()) + duration.inWholeMinutes > 0 -> + stringResource(Res.string.time_minutes_ago, duration.inWholeMinutes.toInt()) + else -> stringResource(Res.string.just_now) } } @@ -314,24 +284,23 @@ private fun formatRelativeDate(dateString: String): String { private fun PreviewDeveloperRepoItem() { GithubStoreTheme { DeveloperRepoItem( - repository = - DeveloperRepository( - id = 1, - name = "awesome-kotlin-app", - fullName = "developer/awesome-kotlin-app", - description = "An amazing Kotlin Multiplatform application that demonstrates modern Android development", - htmlUrl = "", - stargazersCount = 2340, - forksCount = 456, - openIssuesCount = 23, - language = "Kotlin", - hasReleases = true, - hasInstallableAssets = true, - isInstalled = true, - isFavorite = false, - latestVersion = "v1.5.2", - updatedAt = Clock.System.now().toString(), - ), + repository = DeveloperRepository( + id = 1, + name = "awesome-kotlin-app", + fullName = "developer/awesome-kotlin-app", + description = "An amazing Kotlin Multiplatform application that demonstrates modern Android development", + htmlUrl = "", + stargazersCount = 2340, + forksCount = 456, + openIssuesCount = 23, + language = "Kotlin", + hasReleases = true, + hasInstallableAssets = true, + isInstalled = true, + isFavorite = false, + latestVersion = "v1.5.2", + updatedAt = Clock.System.now().toString(), + ), onItemClick = {}, onToggleFavorite = {}, ) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt index 2af618a1f..23a79c937 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt @@ -1,7 +1,11 @@ package zed.rainxch.devprofile.presentation.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -11,15 +15,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Business import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Tag -import androidx.compose.material3.AssistChip -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -27,144 +31,113 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.coil3.CoilImage -import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.devprofile.presentation.DeveloperProfileAction -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun ProfileInfoCard( profile: DeveloperProfile, onAction: (DeveloperProfileAction) -> Unit, ) { - ExpressiveCard { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + ) { + Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top, ) { CoilImage( imageModel = { profile.avatarUrl }, - modifier = - Modifier - .size(80.dp) - .clip(CircleShape), - imageOptions = - ImageOptions( - contentScale = ContentScale.Crop, - ), + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + imageOptions = ImageOptions(contentScale = ContentScale.Crop), ) - - Spacer(modifier = Modifier.width(16.dp)) - + Spacer(Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = profile.name ?: profile.login, maxLines = 2, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ), color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Ellipsis, ) - - Spacer(Modifier.height(4.dp)) - Text( text = "@${profile.login}", maxLines = 1, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + profile.location?.takeIf { it.isNotBlank() }?.let { location -> + Spacer(Modifier.height(6.dp)) + InfoChip(icon = Icons.Default.LocationOn, text = location) + } + } + } + profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> + Spacer(Modifier.height(10.dp)) + Text( + text = bio, + maxLines = 4, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } - profile.location?.let { location -> - Spacer(Modifier.height(8.dp)) - - InfoChip( - icon = Icons.Default.LocationOn, - text = location, + val hasMetaChips = profile.company != null || + profile.blog?.isNotBlank() == true || + profile.twitterUsername != null + if (hasMetaChips) { + Spacer(Modifier.height(12.dp)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + profile.company?.let { company -> + StaticChip(icon = Icons.Default.Business, text = company) + } + profile.blog?.takeIf { it.isNotBlank() }?.let { blog -> + val displayUrl = blog.removePrefix("https://").removePrefix("http://") + ClickableChip( + icon = Icons.Default.Link, + text = displayUrl, + onClick = { + val url = if (!blog.startsWith("http")) "https://$blog" else blog + onAction(DeveloperProfileAction.OnOpenLink(url)) + }, ) } - - profile.bio?.let { bio -> - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = bio, - maxLines = 4, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + profile.twitterUsername?.let { twitter -> + ClickableChip( + icon = Icons.Default.Tag, + text = "@$twitter", + onClick = { + onAction(DeveloperProfileAction.OnOpenLink("https://twitter.com/$twitter")) + }, ) } } } - - Spacer(modifier = Modifier.height(12.dp)) - - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - itemVerticalAlignment = Alignment.CenterVertically, - ) { - profile.company?.let { company -> - InfoChip( - icon = Icons.Default.Business, - text = company, - ) - } - - profile.blog?.takeIf { it.isNotBlank() }?.let { blog -> - val displayUrl = blog.removePrefix("https://").removePrefix("http://") - AssistChip( - onClick = { - val url = if (!blog.startsWith("http")) "https://$blog" else blog - onAction(DeveloperProfileAction.OnOpenLink(url)) - }, - label = { - Text( - text = displayUrl, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Link, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - }, - ) - } - - profile.twitterUsername?.let { twitter -> - AssistChip( - onClick = { - onAction(DeveloperProfileAction.OnOpenLink("https://twitter.com/$twitter")) - }, - label = { - Text( - text = "@$twitter", - style = MaterialTheme.typography.labelMedium, - ) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Tag, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - }, - ) - } - } } } } @@ -181,15 +154,79 @@ private fun InfoChip( Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( text = text, maxLines = 1, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } + +@Composable +private fun StaticChip(icon: ImageVector, text: String) { + Surface( + shape = RoundedCornerShape(50), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + border = BorderStroke( + width = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ), + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = text, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun ClickableChip(icon: ImageVector, text: String, onClick: () -> Unit) { + Surface( + onClick = onClick, + shape = RoundedCornerShape(50), + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), + ), + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(13.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = text, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/StatsRow.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/StatsRow.kt index e15d6c9d3..9042cced9 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/StatsRow.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/StatsRow.kt @@ -1,20 +1,22 @@ package zed.rainxch.devprofile.presentation.components +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.githubstore.core.presentation.res.* @@ -25,19 +27,17 @@ fun StatsRow(profile: DeveloperProfile) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - StatCard( + StatPill( label = stringResource(Res.string.repositories), value = profile.publicRepos.toString(), modifier = Modifier.weight(1f), ) - - StatCard( + StatPill( label = stringResource(Res.string.followers), value = formatCount(profile.followers), modifier = Modifier.weight(1f), ) - - StatCard( + StatPill( label = stringResource(Res.string.following), value = formatCount(profile.following), modifier = Modifier.weight(1f), @@ -45,35 +45,43 @@ fun StatsRow(profile: DeveloperProfile) { } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun StatCard( +private fun StatPill( label: String, value: String, modifier: Modifier = Modifier, ) { - ExpressiveCard(modifier = modifier) { + Surface( + modifier = modifier, + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), + ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 6.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = value, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - style = MaterialTheme.typography.titleLarge, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface, ) - Text( text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, - style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt index 3485fdf72..c861a774b 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt @@ -23,4 +23,6 @@ sealed interface ProfileAction { data object OnAnnouncementsClick : ProfileAction data object OnAnnouncementsLongClick : ProfileAction + + data object OnTweaksClick : ProfileAction } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 3c7cd7ed1..2c8545a4d 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -53,6 +53,7 @@ fun ProfileRoot( onPreviewWhatsNewSheet: () -> Unit, onNavigateToAnnouncements: () -> Unit, onPreviewAnnouncements: () -> Unit, + onNavigateToTweaks: () -> Unit, hasUnreadAnnouncements: Boolean, viewModel: ProfileViewModel = koinViewModel(), ) { @@ -149,6 +150,10 @@ fun ProfileRoot( onPreviewAnnouncements() } + ProfileAction.OnTweaksClick -> { + onNavigateToTweaks() + } + else -> { viewModel.onAction(action) } @@ -230,9 +235,10 @@ private fun TopAppBar() { title = { Text( text = stringResource(Res.string.profile_title), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, ) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 0f0c42184..5f6bb4003 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -148,6 +148,10 @@ class ProfileViewModel( ProfileAction.OnAnnouncementsLongClick -> { } + + ProfileAction.OnTweaksClick -> { + + } } } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt index c974ac1a0..047cfb481 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt @@ -1,8 +1,11 @@ package zed.rainxch.profile.presentation.components.sections +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -10,107 +13,58 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.profile.presentation.ProfileAction -@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.logout(onAction: (ProfileAction) -> Unit) { item { Spacer(Modifier.height(8.dp)) - - ElevatedCard( + Surface( modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - ), - shape = RoundedCornerShape(32.dp), - onClick = { - onAction(ProfileAction.OnLogoutClick) - }, - ) { - AccountItem( - icon = Icons.AutoMirrored.Filled.Logout, - title = stringResource(Res.string.logout), - actions = { - IconButton( - onClick = { - onAction(ProfileAction.OnLogoutClick) - }, - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } - }, - ) - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun AccountItem( - icon: ImageVector, - title: String, - actions: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { }, - colors = - IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), + shape = RoundedCornerShape(50), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.55f), + ), + onClick = { onAction(ProfileAction.OnLogoutClick) }, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) + Box( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 14.dp, horizontal = 18.dp), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Logout, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = stringResource(Res.string.logout), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.error, + ) + } + } } - - Text( - text = title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - modifier = Modifier.weight(1f), - ) - - actions.invoke() } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt index c7cb46dd0..4528d8d55 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt @@ -2,11 +2,13 @@ package zed.rainxch.profile.presentation.components.sections import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -17,30 +19,31 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.domain.model.UserProfile import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.theme.GithubStoreTheme +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.core.domain.model.UserProfile import zed.rainxch.profile.presentation.ProfileAction import zed.rainxch.profile.presentation.ProfileState -@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.accountSection( state: ProfileState, onAction: (ProfileAction) -> Unit, @@ -48,91 +51,77 @@ fun LazyListScope.accountSection( item { Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { if (state.userProfile == null) { - Icon( - imageVector = Icons.Filled.AccountCircle, - contentDescription = null, - modifier = - Modifier - .size(100.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(20.dp), - tint = MaterialTheme.colorScheme.onSurface, - ) + Box( + modifier = Modifier + .size(112.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.AccountCircle, + contentDescription = null, + modifier = Modifier.size(76.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } else { GitHubStoreImage( - imageModel = { - state.userProfile.imageUrl - }, - modifier = - Modifier - .size(128.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), + imageModel = { state.userProfile.imageUrl }, + modifier = Modifier + .size(112.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + extractDominantFor = state.userProfile.imageUrl, ) - - Spacer(Modifier.height(8.dp)) } + Spacer(Modifier.height(8.dp)) + if (state.userProfile != null) { - val displayName = - state.userProfile.name.takeIf { it.isNotBlank() } - ?: state.userProfile.username + val displayName = state.userProfile.name.takeIf { it.isNotBlank() } + ?: state.userProfile.username Text( text = displayName, - style = MaterialTheme.typography.titleLargeEmphasized, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - Text( text = "@${state.userProfile.username}", - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) - - state.userProfile.bio?.let { bio -> + state.userProfile.bio?.takeIf { it.isNotBlank() }?.let { bio -> + Spacer(Modifier.height(4.dp)) Text( text = bio, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, + maxLines = 3, + overflow = TextOverflow.Ellipsis, ) } - } else { - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(Res.string.profile_sign_in_title), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(Res.string.profile_sign_in_description), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - } - - if (state.userProfile != null) { Spacer(Modifier.height(16.dp)) Row( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - StatCard( + StatPill( label = stringResource(Res.string.profile_repos), value = state.userProfile.repositoryCount.toString(), modifier = Modifier.weight(1f), @@ -140,83 +129,100 @@ fun LazyListScope.accountSection( onAction(ProfileAction.OnRepositoriesClick(state.userProfile.username)) }, ) - - StatCard( + StatPill( label = stringResource(Res.string.followers), value = state.userProfile.followers.toString(), modifier = Modifier.weight(1f), ) - - StatCard( + StatPill( label = stringResource(Res.string.following), value = state.userProfile.following.toString(), modifier = Modifier.weight(1f), ) } - } - - if (state.userProfile == null) { - Spacer(Modifier.height(8.dp)) - - GithubStoreButton( - text = stringResource(Res.string.profile_login), - onClick = { - onAction(ProfileAction.OnLoginClick) - }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), + } else { + Text( + text = stringResource(Res.string.profile_sign_in_title), + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(Res.string.profile_sign_in_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) + Spacer(Modifier.height(16.dp)) + Button( + onClick = { onAction(ProfileAction.OnLoginClick) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .height(50.dp), + shape = RoundedCornerShape(50), + contentPadding = PaddingValues(horizontal = 18.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text( + text = stringResource(Res.string.profile_login), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + } } } } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun StatCard( +private fun StatPill( label: String, value: String, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { - Card( + Surface( modifier = modifier, - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - shape = RoundedCornerShape(32.dp), - border = - BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.secondary, - ), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), onClick = { onClick?.invoke() }, + enabled = onClick != null, ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 6.dp), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = value, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - style = MaterialTheme.typography.titleLargeEmphasized, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface, ) - Text( text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, - style = MaterialTheme.typography.bodyLargeEmphasized, overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -227,10 +233,7 @@ private fun StatCard( fun AccountSectionPreview() { GithubStoreTheme { LazyColumn { - accountSection( - state = ProfileState(), - onAction = { }, - ) + accountSection(state = ProfileState(), onAction = { }) } } } @@ -241,20 +244,18 @@ fun AccountSectionUserPreview() { GithubStoreTheme { LazyColumn { accountSection( - state = - ProfileState( - userProfile = - UserProfile( - id = 1, - imageUrl = "", - name = "Octocat", - username = "the_octocat", - bio = "Language Savant.", - repositoryCount = 8, - followers = 21900, - following = 9, - ), + state = ProfileState( + userProfile = UserProfile( + id = 1, + imageUrl = "", + name = "Octocat", + username = "the_octocat", + bio = "Language Savant.", + repositoryCount = 8, + followers = 21900, + following = 9, ), + ), onAction = { }, ) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt index 4f05d31bc..38c104823 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt @@ -16,26 +16,27 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.filled.Campaign import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Star -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.profile.presentation.ProfileAction @@ -50,66 +51,56 @@ fun LazyListScope.options( icon = Icons.Default.Star, label = stringResource(Res.string.stars), description = stringResource(Res.string.profile_stars_description), - onClick = { - onAction(ProfileAction.OnStarredReposClick) - }, + onClick = { onAction(ProfileAction.OnStarredReposClick) }, ) + Spacer(Modifier.height(8.dp)) } - Spacer(Modifier.height(4.dp)) - OptionCard( icon = Icons.Default.Favorite, label = stringResource(Res.string.favourites), description = stringResource(Res.string.profile_favourites_description), - onClick = { - onAction(ProfileAction.OnFavouriteReposClick) - }, + onClick = { onAction(ProfileAction.OnFavouriteReposClick) }, ) - - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(8.dp)) OptionCard( icon = Icons.Default.Schedule, label = stringResource(Res.string.recently_viewed), description = stringResource(Res.string.profile_recently_viewed_description), - onClick = { - onAction(ProfileAction.OnRecentlyViewedClick) - }, + onClick = { onAction(ProfileAction.OnRecentlyViewedClick) }, ) - - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(8.dp)) OptionCard( icon = Icons.Default.Campaign, label = stringResource(Res.string.whats_new_title), description = stringResource(Res.string.whats_new_profile_description), - onClick = { - onAction(ProfileAction.OnWhatsNewClick) - }, - onLongClick = { - onAction(ProfileAction.OnWhatsNewLongClick) - }, + onClick = { onAction(ProfileAction.OnWhatsNewClick) }, + onLongClick = { onAction(ProfileAction.OnWhatsNewLongClick) }, ) - - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(8.dp)) OptionCard( icon = Icons.Default.Notifications, label = stringResource(Res.string.announcements_title), description = stringResource(Res.string.announcements_profile_description), - onClick = { - onAction(ProfileAction.OnAnnouncementsClick) - }, - onLongClick = { - onAction(ProfileAction.OnAnnouncementsLongClick) - }, + onClick = { onAction(ProfileAction.OnAnnouncementsClick) }, + onLongClick = { onAction(ProfileAction.OnAnnouncementsLongClick) }, hasBadge = hasUnreadAnnouncements, ) + Spacer(Modifier.height(8.dp)) + + OptionCard( + icon = Icons.Default.Tune, + label = stringResource(Res.string.tweaks_title), + description = stringResource(Res.string.profile_tweaks_description), + onClick = { onAction(ProfileAction.OnTweaksClick) }, + ) } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable private fun OptionCard( icon: ImageVector, @@ -120,117 +111,74 @@ private fun OptionCard( onLongClick: (() -> Unit)? = null, hasBadge: Boolean = false, ) { - val cardColors = CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - contentColor = MaterialTheme.colorScheme.onSurface, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = .7f), - disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .7f), - ) - val cardShape = RoundedCornerShape(32.dp) - val cardBorder = BorderStroke( - width = .5.dp, - color = MaterialTheme.colorScheme.surface, - ) - - if (onLongClick != null) { - Card( - modifier = modifier.combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ), - colors = cardColors, - shape = cardShape, - border = cardBorder, - ) { - OptionCardContent( - icon = icon, - label = label, - description = description, - hasBadge = hasBadge - ) - } - return + val clickMod = if (onLongClick != null) { + Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick) + } else { + Modifier.combinedClickable(onClick = onClick) } - - Card( + Surface( modifier = modifier, - colors = cardColors, - onClick = onClick, - shape = cardShape, - border = cardBorder, + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + ), ) { - OptionCardContent( - icon = icon, - label = label, - description = description - ) - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun OptionCardContent( - icon: ImageVector, - label: String, - description: String, - hasBadge: Boolean = false, -) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Box { - Icon( - imageVector = icon, - contentDescription = null, - modifier = - Modifier - .size(36.dp) - .clip(CircleShape) - .background( - Brush.linearGradient( - listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.secondary, - ), - ), - ).padding(6.dp), - tint = MaterialTheme.colorScheme.onPrimary, - ) - if (hasBadge) { - Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.error) - .align(Alignment.TopEnd), + Row( + modifier = clickMod.padding(horizontal = 14.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary, ) + if (hasBadge) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.error), + ) + } } - } - - Column( - modifier = - Modifier - .weight(1f) - .padding(12.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start, - ) { - Text( - text = label, - maxLines = 1, - style = MaterialTheme.typography.titleMedium, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface, - ) - - Text( - text = description, - maxLines = 2, - style = MaterialTheme.typography.bodyLargeEmphasized, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = label, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } From 08451e07837c3315b623c47aa7a69c7a5bc7715c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 18:49:38 +0500 Subject: [PATCH 072/172] docs(tweaks): P12.5 redesign spec and research --- .design/P12_5_ABOUT_PLACEMENT_RESEARCH.md | 59 ++ .design/P12_5_TWEAKS_REDESIGN.md | 974 ++++++++++++++++++++++ .design/P12_5_TWEAKS_RESEARCH_REVIEW.md | 394 +++++++++ .design/P12_5_TWEAKS_STRINGS.csv | 158 ++++ 4 files changed, 1585 insertions(+) create mode 100644 .design/P12_5_ABOUT_PLACEMENT_RESEARCH.md create mode 100644 .design/P12_5_TWEAKS_REDESIGN.md create mode 100644 .design/P12_5_TWEAKS_RESEARCH_REVIEW.md create mode 100644 .design/P12_5_TWEAKS_STRINGS.csv diff --git a/.design/P12_5_ABOUT_PLACEMENT_RESEARCH.md b/.design/P12_5_ABOUT_PLACEMENT_RESEARCH.md new file mode 100644 index 000000000..edc8c19ba --- /dev/null +++ b/.design/P12_5_ABOUT_PLACEMENT_RESEARCH.md @@ -0,0 +1,59 @@ +# About GitHub Store — global placement research + +Follow-up to P12_5_TWEAKS_REDESIGN. Scope: where the "About + Send feedback + Open source licenses + Privacy policy" cluster should live globally — Tweaks vs. Profile vs. a top-level surface vs. platform-native menus. + +## Evidence + +**The dominant industry pattern is "Settings is the home for About."** On both iOS and Android, well-designed consumer apps almost universally tuck About, Licenses, Privacy, and Terms into a leaf node deep inside Settings — typically the very last group on the root Settings screen, often under a heading like "About" or "Help & About." Feedback is the one item that frequently breaks out, because feedback is a *task* (user wants to do something) while About/Licenses/Privacy are *references* (user wants to look something up). I'll cite specific apps, distinguishing what I've directly observed from what I'm inferring. + +**iOS, in-app patterns I'm confident about.** *Bitwarden* (iOS): the Settings tab is one of five bottom tabs; inside it, an "About" row sits near the bottom, opening a leaf screen that lists version, server URL, "Rate the App," and links to website/help/legal. *1Password 8* (iOS): the gear icon in the top-right of Home opens Settings; "About" and "Tell us what you think" are both rows in the Settings root list, alongside Security, Appearance, etc. *Tailscale* (iOS): the gear/Settings tab contains "About Tailscale" as a leaf row; "Send feedback" is a separate Settings row, not buried in About. *Discord* (iOS): User Settings (the avatar in the bottom nav) has a long scrollable list; "Acknowledgements," "Open Source Licenses," and "Privacy Policy" all live near the bottom of that same scroll. *Slack* (iOS): per-workspace and global preferences both live in the "You" tab; "Help" opens a sub-screen that contains "Contact us" (feedback) and "Privacy & terms." *Apple Music* in-app has minimal About surface — version info lives in the system Settings app at *Settings → General → About* and at *Settings → Music*, not inside the Music app itself; this is the Apple-platform convention for first-party apps but doesn't generalize to third-party apps. *Notes* follows the same Apple-first-party pattern. + +**Android, in-app patterns I'm confident about.** *Bitwarden* (Android): identical to iOS — Settings tab → About row at the bottom. *1Password* (Android): hamburger-style avatar menu → Settings → About row. *Tailscale* (Android): Settings screen → "About Tailscale" leaf, plus a separate "Send feedback" row. *Discord* (Android): User Settings (bottom-right avatar) → scrollable list → "About" group near the bottom containing Acknowledgements, Licenses, Terms, Privacy Policy. *Slack* (Android): "You" tab → Preferences → Help → Contact Us / Privacy. *Google apps* (Gmail, Maps, Drive, Photos): consistent pattern — avatar in top-right opens an account sheet; that sheet has a "Settings" entry; inside Settings, the last group is always "About, terms & privacy" with sub-rows. This is the strongest cross-app convention on Android and is documented in Google's Material guidelines for settings IA. + +**Desktop, in-app patterns I'm confident about.** *1Password 8* (macOS + Windows): macOS uses the standard `App → About 1Password` menu (OS-injected app menu); Windows uses `Help → About 1Password` in the in-app menu bar; Linux mirrors Windows. *Slack* desktop: `Help → About Slack` on Windows/Linux, `Slack → About Slack` on macOS; "Send Feedback" is *also* in the Help menu, not in Preferences. *Discord* desktop: `Help` is exposed only on Windows/Linux; on macOS About lives in the standard app menu. In-app, Discord also keeps Acknowledgements/Licenses inside User Settings → bottom of list. *VS Code*: canonical example — `Help → About` on Windows/Linux, `Code → About Visual Studio Code` on macOS; "Help → Report Issue" is the feedback entry; licenses live under `Help → Toggle Developer Tools` / files-on-disk rather than a UI surface. *Cloudflare WARP* (desktop): tray icon → Preferences → About tab — no menu bar at all on Windows. **What I don't know with high confidence**: I cannot vouch for the exact placement of About in Apple Music desktop or Notes desktop without verifying, so I'm omitting them rather than guessing. + +**Cross-platform reference: Tailscale.** Tailscale's mobile (iOS+Android) puts About inside Settings as a leaf; on macOS, About is the standard `Tailscale → About Tailscale` app-menu item *and* there's no equivalent inside the menubar popover UI — the popover is intentionally minimal. Windows mirrors macOS through the system tray's right-click menu. So Tailscale accepts platform divergence: mobile = "About inside Settings," desktop = "About in native app menu." Feedback is consistently inside Settings on mobile and only on the website on desktop. + +## Comparison matrix + +| App | Platform | About location | Feedback location | Separation pattern | +|---|---|---|---|---| +| Bitwarden | iOS | Settings tab → About (leaf) | Settings tab → "Get help" / website link | About + Feedback both in Settings root, separate rows | +| Bitwarden | Android | Settings → About | Settings → "Get help" | Same as iOS | +| 1Password 8 | iOS | Settings → About | Settings → "Tell us what you think" | Both in Settings, sibling rows | +| 1Password 8 | macOS desktop | App menu → About 1Password (OS) | Help → "Tell us what you think" | Native menus only | +| Tailscale | iOS/Android | Settings → About Tailscale | Settings → Send feedback | Sibling rows in Settings | +| Tailscale | macOS desktop | App menu → About (OS) | (web only) | Platform-native menu, no in-popover About | +| Discord | iOS/Android | User Settings → Acknowledgements / Licenses / Privacy near bottom | Settings → "Send feedback" + in-app shake-to-feedback | About fragmented across multiple rows at bottom of Settings | +| Discord | Windows/Linux desktop | Help menu → About | Help menu → Submit feedback | Native menu bar | +| Slack | iOS/Android | You tab → Preferences → Help → About + Privacy | You tab → Help → Contact Us | About lives under Help, not at Settings root | +| Slack | macOS/Windows desktop | App menu (mac) or Help menu (win) → About | Help → "Send feedback to Slack" | Native menus | +| Google apps (Gmail/Maps/Drive) | Android | Account sheet → Settings → "About, terms & privacy" | Settings → "Help & feedback" (separate row) | Two distinct rows at Settings bottom | +| VS Code | All desktop | Help → About (win/linux), App menu → About (mac) | Help → Report Issue | Native menu only | +| Cloudflare WARP | Windows desktop | Tray → Preferences → About tab | Preferences → Send feedback | Tabbed Preferences pane, About is its own tab | + +Pattern summary: **on mobile, ~10/10 third-party apps surveyed put About inside Settings, not at a top-level surface.** On desktop, ~10/10 put About in the native menu bar (macOS app menu, Windows/Linux Help menu). Feedback splits roughly 60/40 — most often a sibling Settings row, sometimes inside a "Help" sub-section. None of the surveyed apps put About at a Profile/account-list level; Profile/account is consistently for *the user's stuff*, not *the app's metadata*. + +## Recommendation + +**Option E — refinement of A.** Keep About + Feedback + Licenses + Privacy inside Tweaks as a single "About GitHub Store" leaf sub-screen (matching the architect's current spec), **but**: (1) on desktop, *additionally* surface "About GitHub-Store" via the standard macOS app menu (which the OS already injects today since the JVM Compose `Window` has no `MenuBar` — the system synthesizes an "About App" item that no-ops; we should wire it) and add a minimal `MenuBar { Menu("Help") { Item("About"); Item("Send feedback") } }` for Windows/Linux parity; (2) elevate **Send feedback** to a dedicated, always-visible row at Tweaks hub level — not nested inside the About leaf — because feedback is a task, not a reference, and emergency-feedback paths must be one tap, not two. + +Rationale across the four constraints: **emergency feedback** — Feedback at Tweaks hub level keeps it 2 taps from any screen (bottom nav → Profile → Tweaks → Feedback row is 3; we should also keep the existing entry on the long Tweaks scroll, just move it up). **Mobile-vs-desktop parity** — mobile follows the universal "Settings → About leaf" convention (Bitwarden/1Password/Tailscale/Discord/Google), desktop follows the equally universal "native menu bar → About/Help" convention. This is *not* divergence; it's correctly matching each platform's idiom, the same trade Tailscale and 1Password make. **Discoverability** — About inside Tweaks is exactly where 9/10 users will look on mobile; on desktop, the Help menu is the trained-behavior location since Windows 95 and macOS Classic. **Navigation depth** — Profile → Tweaks → About is two taps from the bottom nav, the same depth as Bitwarden Settings tab → About; Option B (top-level About from Profile) would actually be the *outlier* relative to industry practice and risk making the Profile list semantically muddled, exactly the concern the user flagged. + +Reject A as-spec'd because feedback should not be buried inside the About leaf. Reject B because no surveyed app elevates About to a top-level surface — it's a leaf node everywhere. Reject C because Profile-as-account-stuff is the cleaner semantic and matches Bitwarden/Slack/Google. Reject D (mobile "i" icon in topbar) because it has no precedent in the surveyed apps and would be undiscoverable on mobile. + +## Stretch — desktop-specific consideration + +**Current state, verified in `composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt`:** the `Window { … }` block has no `MenuBar` content. There is no in-app menu bar on Windows or Linux today. On macOS, the JVM hosts a default app menu strip (the OS forces every Java/JVM app to have one), which shows a stock "About GitHub-Store" item that, without explicit handling, opens a default dialog with the app's name only. + +**Implication for the recommendation:** wiring this is cheap and high-impact on desktop. Add a `MenuBar` to the `Window`: + +- macOS: override the default About handler via `Desktop.getDesktop().setAboutHandler { … }` to open our in-app About sheet (the existing `java.awt.Desktop` API the project already uses for URI handling). The Apple menu's About item then routes to our content. No separate Help menu needed — macOS users expect About in the app menu. +- Windows/Linux: provide a minimal `MenuBar { Menu("Help") { Item("About GitHub-Store"); Item("Send feedback…"); Item("Open source licenses"); Item("Privacy policy") } }`. This is the convention every long-tail desktop app follows (VS Code, Slack, Discord, 1Password). Both items deep-link into the same Tweaks → About leaf used on mobile, so there's a single source of truth. + +This makes the desktop story idiomatic without forking the underlying screen — same Compose UI, additional entry points. Net code: ~25 lines in `DesktopApp.kt`, zero changes to mobile. + +## Open questions back to user + +1. **Profile-list "About" shortcut, yes/no?** Industry evidence says no (Profile = user-account stuff), but you specifically asked about it. Are you willing to follow the convention (About lives only inside Tweaks on mobile), or do you want a duplicate "About" row in the Profile screen as a discoverability hedge? +2. **Feedback elevation scope:** should "Send feedback" appear *only* at Tweaks hub level (one path), or also as a row in the new `About` leaf for redundancy (two paths)? Two paths matches Discord/Slack; one path matches 1Password/Tailscale. diff --git a/.design/P12_5_TWEAKS_REDESIGN.md b/.design/P12_5_TWEAKS_REDESIGN.md new file mode 100644 index 000000000..8a3b9e78a --- /dev/null +++ b/.design/P12_5_TWEAKS_REDESIGN.md @@ -0,0 +1,974 @@ +# P12.5 — Tweaks Redesign (V2) + +> Hub-and-spoke IA for the Tweaks feature. Replaces the single-scroll mega-list in `feature/tweaks/presentation/TweaksRoot.kt` with a category hub + dedicated drill-in screens. Reuses the existing design-system vocabulary (`Radii.row`, `WonkySquircleShape.*`, outlined Surface row, `FloatingPill`, `SectionText`/`Squiggle`, `ToggleSettingCard`, `GhsBottomSheet`, `GhsConfirmDialog`, `GhsDropdownMenu`, `PlatformGlyph`). No new fonts. No square corners. No KDoc. +> +> Author: ArchitectUX. Status: V2, post UX-Research critique + product-owner decisions. Supersedes V1 in full. + +--- + +## 0. Change log — V1 → V2 + +Citations point to `.design/P12_5_TWEAKS_RESEARCH_REVIEW.md` (review) and `.design/P12_5_ABOUT_PLACEMENT_RESEARCH.md` (placement). + +| # | Area | V1 said | V2 says | Driver | +|---|---|---|---|---| +| 1 | Telemetry UI | absent | first-class opt-out, lives in new **Privacy** sub-screen | review C1 | +| 2 | Library cleanup | one screen mixing cache + clipboard + history + telemetry-shaped concerns | **split** into **Storage** (APK cache) + **Privacy** (telemetry, clipboard, hide-seen, viewed history) | review M1 | +| 3 | About leaf | "About" sub-screen with feedback nested inside | **App info** sub-screen (About + Licenses + Privacy policy + version) **plus** dedicated **Send feedback** hub row at bottom; desktop also gains `MenuBar` | placement Option E + review M7 | +| 4 | Master proxy | derived from per-scope equality at load | **persisted** as its own record + `useMaster: Boolean` per scope; one-time migration | review C2 | +| 5 | "Direct" rename | `proxy_none` → "Direct" | `proxy_none` → **"No proxy"** (mode pill + body copy updated everywhere) | review C3 | +| 6 | Search in hub | rejected | **shipped**, `WonkySquircleShape.Search` field under topbar, filters by title + subtitle; deep-link routes spec'd as a follow-up | review C5 | +| 7 | Empty / loading / error | partial per screen | every sub-screen carries a standardized states sub-section; reversible → snackbar+Undo, destructive → `GhsConfirmDialog`, scope-changing → snackbar confirming scope | review C4 | +| 8 | Override toggle | "Use main connection" switch | 2-segment chooser per scope: `[ Use main ] [ Custom… ]` | review M2 | +| 9 | Proxy modes | Direct / System / HTTP / SOCKS | **No proxy** / System / **HTTP/HTTPS** / **SOCKS5** + "Paste full URL" affordance; PAC explicit v1 non-goal | review M3 | +| 10 | Border contrast | `outlineVariant.copy(alpha = 0.55f)` | **`outline`** at full opacity (with documented per-palette contrast audit gate before merge) | review M4 | +| 11 | Tap targets / a11y | unspec'd for nested icon buttons | every interactive 48dp min; explicit nested-click pattern; row semantics + status pill `liveRegion`; chevron `contentDescription = null` | review M5 | +| 12 | i18n | English-only rewrite | mandate `pluralStringResource` for counted labels; 32-char subtitle cap with rightmost-token drop on overflow; English-first ship policy + translator CSV handoff | review M6 | +| 13 | Cross-platform rows | hidden on inapplicable platform | shown with `tertiaryContainer` "Android only" / "Desktop only" subtitle badge; clicking routes to centered empty state | review M8 | +| 14 | Hub blocking | 4 blocks for 10 rows | **5 blocks** for 11 rows (Look & feel, Connectivity, Installs & updates, Privacy & data, App); Translation moves into Connectivity; Access tokens into Privacy & data | review M9 (trimmed) | +| 15 | Restart UX | snackbar local to Language screen | **persistent banner** sourced from `TweaksRepository.needsRestartReasons`; shows on hub + every sub-screen until restart | review M10 | +| 16 | Nits | various | applied N1–N12 (see §6 + per-screen specs) | review nits | +| 17 | Connection mode label | "Direct" | "No proxy" | review C3 | +| 18 | Update interval | 3h / 6h / 12h / 24h | "Every 6 hours / Every 12 hours / Daily / Manual only" (drops 3h per rate-limit risk) | review N3 | +| 19 | Installer rename | "System installer (Default)" | "System installer" | review N7 | +| 20 | Licenses row | placeholder, hideable | **shipped**, sourced from `gradle-license-plugin` JSON; row in App info | review N8 | +| 21 | Icon tile tinting | unstated | explicitly **uniform** `surfaceContainerHigh` tile with `onSurfaceVariant` icon tint across all rows | review N9 | +| 22 | "Follow system" subtitle | static "Follow system" | "Follow system · en-US" (resolved tag in parens) | review N12 | + +V1 strengths preserved: outlined `Radii.row` vocabulary across all sub-screens, sentence-case section headers (no more `.uppercase()`), dynamic-state hub subtitles, §3.5 Translation provider radio redesign, empty-state framing for custom forges, §7.4 explicit "did not change" discipline (now §7.6). + +--- + +## 1. Information architecture + +### 1.1 Final category list (the Tweaks hub) + +The hub is grouped into **5 visual blocks**, 11 entry rows total. Each block has its own `SectionText` + `Squiggle` underline, then a stack of entry rows. No top-level "Settings" section header — the topbar already says "Tweaks". + +| Block | Order | Entry row | Icon (Material outlined) | Subtitle (dynamic state) | Drill-in | Platforms | +|---|---|---|---|---|---|---| +| **Look & feel** | 1 | Appearance | `Palette` | Palette + mode, e.g. "Nord · Dark" | `TweaksAppearanceScreen` | All | +| | 2 | Language | `Translate` | Language name, e.g. "English (US)" or "Follow system · en-US" | `TweaksLanguageScreen` | All | +| **Connectivity** | 3 | Connection | `Wifi` | "No proxy" / "HTTP 127.0.0.1:1080" / "System proxy" / "127.0.0.1:1080 · 1 override" | `TweaksConnectionScreen` | All | +| | 4 | Sources | `Hub` | "GitHub + N forges" | `TweaksSourcesScreen` | All | +| | 5 | Translation | `GTranslate` | Provider name + auto state, e.g. "DeepL · auto on" | `TweaksTranslationScreen` | All | +| **Installs & updates** | 6 | Install method | `InstallMobile` | Installer + Ready / Needs permission badge | `TweaksInstallScreen` | All (Android-only behavior; desktop shows badge) | +| | 7 | Update behavior | `Update` | "Every 6 hours" / "Manual only" / "Check failed" badge | `TweaksUpdatesScreen` | All | +| **Privacy & data** | 8 | Storage | `Inventory2` | Live cache size, e.g. "Downloads: 124 MB" | `TweaksStorageScreen` | All | +| | 9 | Privacy | `PrivacyTip` | Compact state, e.g. "Telemetry off · clipboard on" | `TweaksPrivacyScreen` | All | +| | 10 | Access tokens | `VpnKey` | "N tokens" (plural-aware) / "No tokens yet" | `HostTokensScreen` (existing) | All | +| **App** | 11 | App info | `Info` | App version, e.g. "1.8.3" | `TweaksAppInfoScreen` | All | +| | 12 | Send feedback | `Feedback` | "We read every report." | (opens `FeedbackBottomSheet` directly) | All | + +Notes on the IA: + +- **Translation moves to Connectivity.** It has network credentials + auto-translate state — tighter fit than "Look & feel." Review M9 picked at this; we agree with the move, but trimmed to 5 blocks. +- **Access tokens lives under Privacy & data.** They're sensitive credentials, not network plumbing. Reads "Privacy & data → Access tokens" much more naturally than "Network & data → Access tokens." +- **Install method shows on both platforms** with platform badging (M8 fix). Desktop click → centered empty state explaining the constraint. +- **Send feedback is a hub row, not nested.** Per placement Option E + review M7. One tap from the hub. +- **Search-in-hub** sits under the topbar, above the first block (see §2.5). + +### 1.2 Platform conditional rules + +- **Install method**: row visible everywhere. On desktop, subtitle badge "Android only" (`tertiaryContainer` pill). Clicking routes to a centered empty state. +- **Update behavior**: cross-platform. On desktop, the WorkManager interval picker is replaced by a single "Check on launch" toggle (see §3.7). +- **Storage**: fully cross-platform (per-platform downloads dir). +- **Appearance**: shows scrollbar + content width only on desktop. AMOLED only when dark mode is resolved on either platform. +- **Privacy**: cross-platform. Telemetry opt-out shows on both platforms. +- **App info**: cross-platform. Identical content; desktop additionally exposes via `MenuBar` (see §8). +- **Send feedback**: cross-platform. Same `FeedbackBottomSheet` on both. + +### 1.3 What stays where + +| Existing thing | New home | +|---|---| +| `MirrorPickerScreen` | Reached from **Sources** (existing nav route preserved) | +| `HostTokensScreen` | Reached from **Access tokens** hub row (existing nav route preserved) | +| `SkippedUpdatesScreen` | Reached from **Update behavior** | +| `HiddenRepositoriesScreen` | Reached from **Update behavior** | +| `WhatsNewHistoryScreen` | Reached from **App info** | +| `FeedbackBottomSheet` | Opens **directly** from the Send feedback hub row (no intermediate screen) | +| `CustomForgesDialog` | Replaced by `CustomForgesSheet` opened from **Sources** | +| `ClearDownloadsDialog` | Opened from **Storage** | +| Clipboard / hide-seen / viewed-history controls | Moved into **Privacy** sub-screen | +| Telemetry opt-out (new UI) | **Privacy** sub-screen | +| About / Licenses / Privacy policy link | Folded into **App info** sub-screen | + +--- + +## 2. Hub screen — `TweaksScreen` (the new root) + +### 2.1 Topbar + +**Pattern**: plain large title, no FloatingPill. + +Rationale: FloatingPill is the "in-content overlay" pattern used in Search + Auth where the topbar floats over scrolling hero content. Tweaks has no hero. Tweaks is a deep settings hub — it wants a steady, anchored title. + +Spec: + +- Material 3 `LargeTopAppBar` with collapsing behavior (`TopAppBarDefaults.exitUntilCollapsedScrollBehavior`). +- Title: "Tweaks" (`Res.string.tweaks_title`). +- Title font: Geist SemiBold, 28sp expanded → 20sp collapsed. +- Leading: back arrow, contentDescription "Back". +- Trailing: none. Search lives below the topbar (§2.5), not in it. +- Container color: `colorScheme.background`. + +### 2.2 Category entry row — `TweaksEntryRow` + +The hub is a list of 11 identical rows grouped into 5 sub-sections. The row is the shared primitive, lives at `feature/tweaks/presentation/components/TweaksEntryRow.kt`. + +**Shape & color**: + +- Outer `Surface(shape = Radii.row, color = colorScheme.surfaceContainerLow, border = BorderStroke(1.dp, colorScheme.outline))`. +- **Border**: `colorScheme.outline` at **full opacity**. Drops V1's `outlineVariant.copy(alpha = 0.55f)` per review M4 (failed WCAG 1.4.11 on Cream light). Implementer must screenshot Nord/Cream/Forest/Plum × Light/Dark/AMOLED matrix and confirm border passes 3:1 contrast before merge. Fallback if `outline` reads too heavy on dark: `outline.copy(alpha = 0.55f)` but only after measured contrast > 3:1 on Cream light. +- Inner `Row`, `Modifier.fillMaxWidth().clickable { onClick() }.padding(horizontal = 16.dp, vertical = 14.dp)`. + +**Visual zones**, left → right: + +1. **Icon tile**: 40.dp square clipped to `Radii.chip`, background **uniformly** `colorScheme.surfaceContainerHigh`, padded 8.dp, icon tinted `onSurfaceVariant`. Never per-row colored. (review N9) +2. **Two-line text column** (`Modifier.weight(1f)`): + - Title: `titleMedium`, `onSurface`, `FontWeight.SemiBold`. + - Subtitle: `bodySmall`, `onSurfaceVariant`, max 1 line, ellipsize end. **Dynamic** — reflects current domain state. +3. **Optional trailing badge slot** (status pill): `tertiaryContainer` background, `Radii.chip` shape, `labelSmall`, `FontWeight.Medium`. Used by Install method ("Ready" / "Needs permission" / "Android only"), Update behavior ("Check failed"). When present, the row's `Row` is `semantics { liveRegion = LiveRegionMode.Polite }` so TalkBack announces re-evaluation when state changes. +4. **Trailing chevron**: `Icons.AutoMirrored.Filled.ChevronRight`, 24.dp, tinted `onSurfaceVariant`, `contentDescription = null` (decorative). + +**Tap targets & nested clicks** (review M5): + +- Entire row is 48dp+ min height (icon tile 40.dp + 14.dp padding × 2 = 68dp). +- Any inner `IconButton` (e.g. delete on Custom forges row) wraps in `Modifier.size(48.dp).clip(Radii.chip)`. +- Compose 1.5+: outer `Modifier.clickable` and inner `IconButton.onClick` correctly dispatch to the nearest handler — no manual `consumeWindowInsets` needed. Implementer must verify in a smoke test (tap the inner icon, confirm only its handler fires). + +**A11y semantics** (review M5): + +``` +Modifier.semantics { + role = Role.Button + contentDescription = "$title. $subtitle. Double-tap to open." +} +``` + +Chevron `contentDescription = null`. Status pill participates via `liveRegion = Polite`. Override segment (in Connection sub-screen) announces "Discovery override on, custom proxy settings for Discovery" when expanding. + +**Press feedback**: standard ripple inside the squircle border. Spring scale-on-press: 0.97× on Android, 0.985× on desktop (review N1), `MediumBouncy` per D10. + +**Component sketch**: + +``` +@Composable +fun TweaksEntryRow( + title: String, + icon: ImageVector, + subtitle: String? = null, + badge: (@Composable () -> Unit)? = null, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) +``` + +### 2.3 Restart-required banner (new — review M10) + +`TweaksRepository` exposes `needsRestartReasons: StateFlow>` and `clearRestartReasons()`. Reasons enum: `LANGUAGE`, `THEME_MIGRATION`, `TELEMETRY_TOGGLE`. + +Banner placement: + +- **Hub**: top of the `LazyColumn`, above the search field. Sticky during collapsing topbar scroll. +- **Sub-screens**: top of each sub-screen's `LazyColumn`, above the first card. + +Banner visual: + +- `Surface(shape = Radii.row, color = colorScheme.tertiaryContainer, border = BorderStroke(1.dp, colorScheme.outline))`. +- Body: "Some changes need a restart to apply." If multiple reasons, append clarifier line: "Affected: language, theme." +- Two trailing buttons: "Restart now" (`WonkySquircleShape.CtaPrimary`, `FilledTonalButton`) + "Later" (text button). "Later" dismisses for the current session only — banner returns next launch until restart. +- "Restart now" routes to the existing app-restart codepath the Language flow already uses. + +Banner disappears only when `needsRestartReasons` is empty (i.e. app process restarted). + +### 2.4 Section header + +Reuses `SectionHeader` from `feature/tweaks/presentation/components/SectionText.kt` (`titleLarge` + `Squiggle`). Section labels (sentence-case, **never** uppercase): + +- "Look & feel" +- "Connectivity" +- "Installs & updates" +- "Privacy & data" +- "App" + +### 2.5 Search-within-hub (new — review C5) + +A single inline filter field, lives between the restart banner (if visible) and the first section header. + +- `OutlinedTextField`, shape **`WonkySquircleShape.Search`**, leading icon `Icons.Default.Search`, trailing clear icon (when non-empty), placeholder "Search settings" (sentence-case, no period). +- ~20 lines of Compose. No new VM logic — pure presentational `remember { mutableStateOf("") }`. Filters the static row list in `TweaksScreen` by `title.contains(query, ignoreCase = true) || subtitle?.contains(query, ignoreCase = true) == true`. +- When query is non-empty, section headers hide and rows render in a flat list. +- When query yields zero matches: empty-state card (outlined `Radii.row`), `Icons.Outlined.SearchOff` icon tile + "No settings match ''." + +Deep-link routes (`githubstore://tweaks/`) are **spec'd here as a follow-up ticket** (P12.5.1, post-launch). Not in v1 scope. Route names when implemented: `appearance`, `language`, `connection`, `sources`, `translation`, `install-method`, `updates`, `storage`, `privacy`, `access-tokens`, `app-info`, `send-feedback`. + +### 2.6 Vertical rhythm + +``` +LazyColumn(contentPadding = 16.dp horizontal, top = 8.dp, bottom = bottomNavHeight + 32.dp) + restart banner (if reasons non-empty) + Spacer(12.dp) + search field + Spacer(16.dp) + section header 1 + Spacer(8.dp) + entry rows (separated by Spacer(8.dp)) + Spacer(24.dp) + section header 2 + ... +``` + +### 2.7 Empty / loading / error states (hub) + +- **Empty**: not possible — every row is always present. +- **Loading**: hub renders synchronously from `TweaksState`. The only field that can lag is App info's `versionName`; placeholder `"—"` until populated. +- **Search empty result**: see §2.5. +- **Error**: hub has no async sources of its own; per-row dynamic subtitles read from already-loaded `TweaksState`. If a subtitle's underlying flow errors (e.g. cache size read fails), the row still renders with a graceful fallback subtitle ("Tap to manage"). + +--- + +## 3. Per-sub-screen specs + +Conventions shared by every sub-screen: + +- **Topbar**: Material 3 `MediumTopAppBar` + back arrow + title. No subtitle line in the bar. +- **Container**: `Scaffold(containerColor = colorScheme.background)` + `LazyColumn`, `contentPadding = 16.dp` horizontal, 8.dp top, `bottomNavHeight + 32.dp` bottom. +- **Restart banner**: §2.3 banner renders at the top of every sub-screen when `needsRestartReasons` non-empty. +- **Cards**: all use `Radii.row` outlined Surface (`surfaceContainerLow` + full-opacity `outline` border per M4). +- **Section sub-headers inside a sub-screen**: `titleSmall` + `FontWeight.SemiBold`, `onSurface`, padded `start = 4.dp, top = 16.dp, bottom = 8.dp`. No `Squiggle` inside sub-screens. +- **Save state**: per-screen settings persist immediately. Explicit Save buttons only on Translation credentials + Connection proxy editor (validation-gated). +- **Snackbar**: each sub-screen has its own `SnackbarHostState`, subscribed to `TweaksViewModel.events` filtered to events applicable to it. +- **Standardized empty / loading / error pattern**: + - Reversible action → snackbar with Undo. + - Irreversible destructive (clear cache, clear history, remove token, remove forge) → `GhsConfirmDialog`. + - Scope-changing mode switch (proxy override on/off, telemetry off→on) → snackbar confirming scope of change, e.g. "Downloads now uses the main connection." + +### 3.1 Appearance — `TweaksAppearanceScreen` + +**Title**: "Appearance" + +**Layout** (top → bottom): + +1. **Theme card** — outlined `Radii.row`. + - Sub-header: "Theme". + - **Mode segment** (3-way): Light / Dark / Follow system. Uses `ModePillSegment` (§5.1). + - 16.dp spacer. + - **Palette grid** (`FlowRow`): 4 `PaletteSwatch`es — Nord, Cream, Forest, Plum. + - `AnimatedVisibility(resolvedDark)` AMOLED inline toggle. +2. **Text & layout card** — outlined `Radii.row`, inner `Column`. + - Sub-header: "Text & layout". + - Row: System font toggle. Title "Use system font", subtitle "Use your device's default typeface instead of Geist." + - Row (desktop only): Show scrollbar toggle. + - Row (desktop only): Content width segment (`Compact` / `Wide` / `Extra wide`). + +**Empty / loading / error**: no async loads; state is synchronous from `TweaksState.appearance`. Palette change → crossfade (D10) + snackbar "Theme applied." Theme migrations that need restart (rare, only on major palette schema change) push `RestartReason.THEME_MIGRATION` into `needsRestartReasons`. + +**Gotchas**: AMOLED visibility binds to `resolvedDark` (System + dark OS still shows). When mode segment switches to Light explicitly, AMOLED collapses with `AnimatedVisibility(false)`. State machine: AMOLED visible ⇔ `(mode == Dark) || (mode == FollowSystem && systemIsDark)`. (review N11) + +### 3.2 Language — `TweaksLanguageScreen` + +**Title**: "Language" + +**Layout**: + +1. Top intro card (outlined `Radii.row`): + - Title "App language". + - Body "The app restarts when you change this so all screens reload." +2. Search field — `OutlinedTextField`, `WonkySquircleShape.Search` shape, leading search icon, placeholder "Search languages". +3. Language list — each language a `Radii.row` outlined Surface row: + - Language name in its own locale (e.g. "Deutsch", "Polski"). + - Subtitle: English name in parentheses if different + tag (`pl-PL`) in `Geist Mono labelSmall`. + - Trailing `Icons.Default.Check` (24.dp, `colorScheme.primary`) when selected. +4. First row is always "Follow system" with `Icons.Outlined.PhoneAndroid` (Android) / `Icons.Outlined.Computer` (desktop). Subtitle includes the resolved tag, e.g. "Follow system · en-US" (review N12). + +**Validation / events**: selecting a language fires `OnAppLanguageSelected(tag)` → repository sets language **and** pushes `RestartReason.LANGUAGE` into `needsRestartReasons`. The §2.3 restart banner takes over; no snackbar from this screen. + +**Empty / loading / error**: +- Empty list: never (35+ languages baked in). +- Search empty: same pattern as §2.5 ("No languages match ''"). +- Error: not possible (static list). + +### 3.3 Connection — `TweaksConnectionScreen` (proxy redesign) + +This screen is the centerpiece. Full deep-dive in §4. + +### 3.4 Sources — `TweaksSourcesScreen` + +**Title**: "Sources" + +**Layout**: + +1. Top intro card (outlined `Radii.row`): + - Title "Where the app looks for repositories". + - Body "The app searches GitHub by default. You can route it through a regional mirror or add custom Forgejo / Gitea hosts." +2. **Mirror row** (outlined `Radii.row`) → `MirrorPickerScreen`. + - Title "GitHub mirror". Subtitle: "Default (github.com)" or selected mirror display name. +3. Sub-header "Custom forges". + - If `customForgeHosts.isEmpty()`: empty-state outlined row, title "Add a Forgejo or Gitea host", subtitle "We already know about codeberg.org and gitea.com. Add others here.", trailing `Icons.Default.Add`. Opens `CustomForgesSheet`. + - If non-empty: list of host rows. + - Title in `Geist Mono labelLarge`, +1 weight bump for visual parity with proportional text (review N5). + - Subtitle "Added manually" or "Suggested by URL parse". + - Trailing `Icons.Outlined.DeleteOutline` inside a 48dp `IconButton.size(48.dp).clip(Radii.chip)`. On click → `GhsConfirmDialog` ("Remove `git.disroot.org`? Repos hosted there will stop appearing in search."). + - "Add another" row below the list — outlined `Radii.row`, `Icons.Default.Add` icon tile, title "Add another host". Opens `CustomForgesSheet`. + +**`CustomForgesSheet`** (new, replaces `CustomForgesDialog`): + +- `GhsBottomSheet` with `WonkySquircleShape.Sheet`. +- Single `OutlinedTextField` "Hostname", placeholder "code.example.org", shape `WonkySquircleShape.Search`. +- Helper text: "Enter the bare hostname. We'll add https:// and validate it's a Forgejo or Gitea instance." +- Inline error states (existing `customForgeError`). +- CTA: "Add host", full-width `WonkySquircleShape.CtaPrimary`, disabled until non-blank + passes validation. + +**Empty / loading / error**: +- Empty (no forges added): the empty-state row above is the empty state. +- Loading (validating new host): inline progress indicator in the sheet's CTA, button text "Validating…" +- Error: inline `supportingText` on the field; failures during validation surface as a snackbar on the Sources screen after the sheet dismisses, e.g. "Couldn't reach `code.example.org`." +- Reversible host removal: snackbar with Undo (5s window) after `GhsConfirmDialog` confirms. + +### 3.5 Translation — `TweaksTranslationScreen` + +**Title**: "Translation" + +**Layout**: + +1. **Provider card** (outlined `Radii.row`): + - Sub-header: "Provider". + - 5 stacked **provider radio rows**: + - Outlined `Radii.chip` Surface each. Radio button + icon tile + title + 1-line description. + - Google: "Free, no setup." + - DeepL: "High quality. Requires API key." + - Microsoft: "Free tier. No-Trace mode by default." + - LibreTranslate: "Open source. Self-hostable." + - Youdao: "Best for Chinese." + - Selecting a row persists provider choice immediately and reveals credentials card. +2. **Credentials card** (outlined `Radii.row`) — only when provider needs credentials (i.e. not Google). + - Sub-header: "{Provider name} credentials". + - Body: provider-specific help + "Get a free API key →" link. + - Form fields: `OutlinedTextField`s with `Radii.chip` shape. + - Visibility toggles on secret fields. + - Save button: `WonkySquircleShape.CtaPrimary` `FilledTonalButton`, full-width, disabled until form valid. +3. **Auto-translate card** (outlined `Radii.row`): + - Sub-header: "Auto-translate READMEs". + - Toggle: title "Translate READMEs automatically", subtitle "When opening a repo, translate the README into your target language." + - When enabled: target language picker drill-row that **reuses §3.2 list** filtered to `SupportedTranslationLanguages.all` (review N2 — same picker, not a parallel one). + +**Empty / loading / error**: +- No provider selected: Google is always the implicit default; never empty. +- Credentials missing for non-Google: one-line warning inside credentials card "No key yet — using Google as fallback." (`onSurfaceVariant`, no destructive coloring). +- Save error: snackbar with retry, e.g. "Couldn't validate DeepL key. [Retry]" +- Save success: snackbar "DeepL credentials saved." + +### 3.6 Install method — `TweaksInstallScreen` + +**Title**: "Install method" + +**Platform visibility**: hub row shows on both platforms; on desktop the row carries a "Desktop only" → actually "Android only" badge subtitle (review M8). Clicking on desktop routes to a centered empty state (see below). All other content is Android-only. + +**Layout (Android)**: + +1. Top intro card (outlined `Radii.row`): + - Title "How the app installs APKs". + - Body "Choose between the system installer or a silent installer that doesn't ask each time." +2. **Installer picker card** (outlined `Radii.row`): + - Sub-header: "Method". + - 4 vertical radio rows (System / Shizuku / Dhizuku / Root), each with icon tile, title, description, status badge (Ready / Needs permission / Not installed / Not running). Selected state: 2.dp `colorScheme.primary` border on the row + left-edge 4.dp wide primary-tinted bar. + - Title rename per review N7: "System installer" (no "(Default)" suffix). + - When selected installer needs permission, "Grant permission" button: `WonkySquircleShape.CtaPrimary`. +3. **Auto-update card** (outlined `Radii.row`) — only when silent install ready: + - Toggle: "Install updates in the background", subtitle "Apply downloaded updates without notifying you each time." +4. **Attribution card** (outlined `Radii.row`) — only when silent install ready: + - Sub-header: "Pretend the installer is…". + - Body + radio rows + custom field — unchanged from V1. + +**Desktop empty state**: + +- Centered `Icons.Outlined.Computer` (48dp) + title "Install method is Android-only" + body "Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager." + back CTA `WonkySquircleShape.CtaAlt` "Back to Tweaks". + +**Empty / loading / error**: +- Loading installer states: each radio row's status badge defaults to "Checking…" until `InstallerStatusProvider` returns. Badge has `liveRegion = Polite`. +- Permission grant errors: snackbar. +- Attribution custom field error: inline `supportingText`. + +### 3.7 Update behavior — `TweaksUpdatesScreen` + +**Title**: "Update behavior" + +**Layout**: + +1. Battery optimization banner (Android only, when `state.showBatteryOptimizationCard`): outlined `Radii.row` `tertiaryContainer` tinted, Open / Dismiss buttons. +2. **Check for updates card** (outlined `Radii.row`): + - Sub-header: "Checking". + - Toggle: "Check automatically", subtitle "Look for new releases on a schedule." + - When enabled, reveal interval segment. **4 pills** (review N3): "Every 6 hours" / "Every 12 hours" / "Daily" / "Manual only". Drops the 3h option (GitHub rate limits). Disabled-state styling when toggle off. +3. **Pre-releases toggle row**: "Include pre-releases", subtitle "Show alpha and beta tags as available updates." +4. Sub-header "Manage": + - Drill: "Skipped updates" → `SkippedUpdatesScreen`. Subtitle: plural-aware "%d app" / "%d apps" or "Nothing skipped" when 0. + - Drill: "Hidden repositories" → `HiddenRepositoriesScreen`. Subtitle: plural-aware count or "No hidden repos." + +**Desktop**: auto-update card simplifies to a single toggle "Check on launch" (no interval picker, no WorkManager). Skipped + Hidden drill rows still apply. What's-new history moves to **App info** (V1 had it here; V2 moves it because it's reference content about the app, not update behavior). + +**Empty / loading / error**: +- "Manual only" interval = no scheduled check; subtitle on hub reads "Manual checks only." +- Last-check error: hub subtitle pill "Check failed" (`tertiaryContainer`), in-screen banner "Last check failed at 14:02 — [Retry]." +- Plural-aware count subtitles use `pluralStringResource`. + +### 3.8 Storage — `TweaksStorageScreen` (new, split from V1's Library cleanup) + +**Title**: "Storage" + +**Scope**: downloaded APK cache only. Everything else (clipboard, hide-seen, viewed history, telemetry) moves to §3.9 Privacy per review M1. + +**Layout**: + +1. **Downloaded packages card** (outlined `Radii.row`): + - Icon tile: `Icons.Outlined.Inventory2`. + - Title: "Downloaded APKs". + - Body: "We keep installers around so updates resume fast. Clear them anytime." + - Status line in `Geist Mono labelMedium`: "Using: 124 MB" / "Using: 0 B" when empty. + - Trailing primary action: `WonkySquircleShape.CtaAlt` `FilledTonalButton` "Clear", `errorContainer` tinted. **Disabled when size = 0 B**. Opens `ClearDownloadsDialog` (existing). + +**Empty / loading / error**: +- Size = 0 B: status reads "Using: 0 B"; Clear button disabled with `disabledContainerColor` per Material 3 disabled-tonal pattern. No empty-state card needed — "0 B" is itself the empty state. +- Loading size: status reads "Calculating…" with subtle indeterminate text shimmer (existing `TweaksViewModel.OnRefreshCacheSize` initialization period). +- Clear success: snackbar "Cleared 124 MB." +- Clear error: snackbar with retry. + +### 3.9 Privacy — `TweaksPrivacyScreen` (new — review C1 + M1) + +**Title**: "Privacy" + +**Scope** (per product-owner decisions): telemetry opt-out, clipboard detection, hide-seen, viewed-history clear. + +This screen is **not** the home of the *legal* "Privacy policy" link — that lives in §3.11 App info per placement Option E. The naming overlap is intentional: this sub-screen surfaces in-app behavior toggles; App info surfaces the legal document. + +**Layout**: + +1. **Telemetry card** (outlined `Radii.row`, inner Column): + - Sub-header: "Usage data". + - Toggle row: title "Share anonymous usage data", subtitle "Help us understand which features get used." + - Below the toggle, an **expandable "What we collect"** row (chevron icon, `Icons.Default.ExpandMore` rotates 180°). Expanded body is bulleted plain text — app version, OS + platform, feature counts (no repo names, no tokens, no identifiers). Compose `AnimatedVisibility`. + - When toggled off→on, snackbar "Sharing usage data starting next launch." When toggled on→off, snackbar "Usage data sharing stopped. Existing data is dropped." Push `RestartReason.TELEMETRY_TOGGLE` into `needsRestartReasons` so the banner appears (telemetry init runs at app start). +2. **Clipboard card** (outlined `Radii.row`): + - Toggle: title "Detect repo links in clipboard", subtitle "When you copy a github.com or codeberg.org link, we'll prompt to open it." +3. **Browsing history card** (outlined `Radii.row`, inner Column): + - Sub-header: "Browsing history". + - Toggle: title "Hide repos I've already viewed", subtitle "Skip seen repos in feeds and search." + - Drill row (destructive): title "Clear viewed history". Click → `GhsConfirmDialog` ("Clear all viewed history? This won't unstar or unfavorite anything."). On confirm, snackbar with **Undo** for 5s window, then commits irreversibly. (Treats this as reversible-with-Undo rather than purely irreversible — viewed history is non-critical data.) + +**Empty / loading / error**: +- Telemetry expandable: static content; never loading. +- Clipboard / hide-seen toggles: synchronous state; never loading. +- Clear viewed history error: snackbar with retry. + +**Hub subtitle compact state** (for §1.1 row 9): `pluralStringResource`-driven, capped at 32 chars per review M6. Examples: +- "Telemetry off · clipboard on" (28 chars) +- "Telemetry on · 3 toggles on" — drop "clipboard" / "hide-seen" specifics on overflow. + +### 3.10 Access tokens — `HostTokensScreen` (existing) + +No visual changes to the screen contents. Audit pending — confirm `HostTokensScreen` rows use `Radii.row` outlined Surface + new full-opacity `outline` border (review M4). If they don't, refactor as part of P12.5. + +Hub subtitle: plural-aware "%d token" / "%d tokens" (review M6), or "No tokens yet" when empty. + +### 3.11 App info — `TweaksAppInfoScreen` (new — replaces V1 §3.10 About) + +**Title**: "App info" + +**Scope** (per placement Option E): about + licenses + privacy policy link + version metadata. **Does not** include feedback (separate hub row §3.12). **Does not** include in-app privacy toggles (those are §3.9 Privacy). + +**Layout**: + +1. **App identity card** (outlined `Radii.row`, single hero-style card): + - Top-left: 56.dp app icon (load actual app icon, fallback `Icons.Outlined.Store`). + - App name "GitHub Store" (`titleLarge`). + - Version `versionName` in `Geist Mono labelLarge`. Long-press copies to clipboard with snackbar "Version copied." + - Tagline: "Cross-platform app store for GitHub, Codeberg, and Forgejo releases." (xliff-style placeholders for the three product names in translator handoff per review N4.) +2. **Action rows** (each outlined `Radii.row` drill-in row): + - **What's new** — icon `Icons.Outlined.NewReleases`, subtitle "Past release notes." Opens `WhatsNewHistoryScreen`. + - **Open source licenses** — icon `Icons.Outlined.Code`, subtitle "Libraries used in the app." Opens new `LicensesScreen` fed by `gradle-license-plugin` JSON (ship as part of P12.5, not a placeholder per review N8). If `gradle-license-plugin` isn't configured yet, add it in this phase. + - **Privacy policy** — icon `Icons.Outlined.Description` (or `PrivacyTip` if the §3.9 row uses `Shield`), subtitle "View on github-store.org." Opens `github-store.org/privacy` in external browser via `LocalUriHandler`. Distinct from §3.9 Privacy by icon + subtitle copy. + - **Source code on GitHub** — icon GitHub glyph (reuse `PlatformGlyph` GitHub variant; fallback `Icons.Outlined.Code`), subtitle "View this app's source." Opens the repo URL. + +**Empty / loading / error**: +- Version loading: placeholder "—" until populated. +- Licenses JSON unreachable (shouldn't happen since shipped as asset): empty-state card "Couldn't load licenses. [Retry]." +- External URL handlers can fail on desktop without a default browser: snackbar "Couldn't open privacy policy. URL copied to clipboard." with the URL pre-copied as a fallback. + +**Distinction from §3.9 Privacy**: §3.9 = in-app behavior toggles ("what the app does with your data"). §3.11 = static reference content ("what the app is, what its docs say"). They share neither title nor icon set. The hub orders them in different blocks (Privacy & data vs App) so users encounter them with different intent. + +### 3.12 Send feedback — hub row, not a sub-screen + +Per product-owner decision + placement Option E. **Single feedback path** (not duplicated inside App info per researcher's open question). + +- Hub row (§1.1 row 12) tapping opens `FeedbackBottomSheet` directly. No intermediate screen. +- `FeedbackBottomSheet` uses `GhsBottomSheet` with `WonkySquircleShape.Sheet`. +- Behavior unchanged from today — pre-fills user-agent + version metadata. + +**Empty / loading / error**: +- Submit success: sheet dismisses, hub snackbar "Feedback sent. Thanks." +- Submit error: inline error in sheet, retry button. + +### 3.13 (reserved — was §3.13 Privacy in product-owner brief, merged into §3.9 above) + +### 3.14 (reserved — was §3.14 Storage in product-owner brief, merged into §3.8 above) + +--- + +## 4. Proxy redesign (deep dive) + +### 4.1 Pattern — master + per-scope overrides (Option B, persisted) + +Master proxy applies to all 3 scopes by default. Each scope has a `[ Use main ] [ Custom… ]` segment (review M2). "Custom…" reveals a mini-editor; "Use main" hides it. + +Persistence change (review C2): the master config is its **own** `ProxyConfig?` record in `ProxyRepository`, plus a `useMaster: Boolean` per scope. The V1 promise to derive master from per-scope equality is reversed — equality-derivation is racy across DataStore writes and the user can't see "which scope am I testing." Schema migration in §4.5. + +### 4.2 Why master + overrides + +1. ~95% of users want one proxy everywhere. V1's 3-card design forces them to fill the same form 3 times. +2. Per-scope overrides (Tor SOCKS5 for Translation, corporate HTTP for Downloads) are real power-user scenarios. +3. Persisted master = glance-readable hub subtitle: "No proxy" / "HTTP 127.0.0.1:1080" / "HTTP 127.0.0.1:1080 · 1 override". +4. Persisted master = unambiguous "which scope am I testing" — test buttons name the scope. + +### 4.3 The Connection screen, step by step + +**Title**: "Connection" + +**Top intro card** (outlined `Radii.row`): +- Title "How the app reaches the internet". +- Body "Pick a connection mode below. Most people leave this on No proxy." + +**Card 1 — Main connection** (outlined `Radii.row`, inner Column): + +1. Sub-header: "Main connection". +2. **Mode segment** — 4-way `ModePillSegment`: **No proxy** / **System** / **HTTP/HTTPS** / **SOCKS5** (review M3 + C3). + - "No proxy" / "System" → form collapses (no fields to edit). + - "HTTP/HTTPS" / "SOCKS5" → form expands `AnimatedVisibility`. + - Small caption under HTTP/HTTPS pill: "Most corporate proxies." Small caption under SOCKS5: "Tor, SSH tunnels." (review M3) +3. **Form fields** (when expanded): + - Row: Host (weight 2) + Port (weight 1) `OutlinedTextField`s, shape `Radii.chip`, with existing `isLikelyValidProxyHost` validation. + - Username (optional). + - Password (optional) with eye toggle. + - Inline helper: "Applies to all traffic unless overridden below." (review N10 — compressed.) +4. **Paste full URL** affordance (review M3) — text-button below the form fields, `Icons.Outlined.ContentPaste` leading icon, label "Paste full URL". Opens a modal sheet (`GhsBottomSheet`, `WonkySquircleShape.Sheet`) with a single paste field. Parser accepts `scheme://user:pass@host:port` (`scheme ∈ {http, https, socks5}`). On parse success: form fields populate, sheet dismisses, snackbar "Pasted from URL." On parse fail: inline error "Couldn't read that URL." +5. **Test & save row** (HTTP/HTTPS / SOCKS5 only): + - **Test** — `OutlinedButton`, `Radii.chip`, leading `Icons.Default.NetworkCheck`. Label: **"Test main connection"** (review C2 — announce scope). On click: tests against 3 endpoints (search API, download CDN, configured translation provider). Results snackbar is 3-line: "Search ✓ 184 ms · Downloads ✓ 92 ms · Translation ✓ 220 ms" or per-endpoint failures. + - **Save** — `FilledTonalButton`, `WonkySquircleShape.CtaPrimary`, leading `Icons.Default.Save`. Disabled until form valid. Writes to the persisted **master** record. + +Test request honors `HostTokenInterceptor` per review N6 — for private GH Enterprise + PAT users, the test uses the user's PAT. + +**Card 2 — Per-scope overrides** (outlined `Radii.row`, inner Column): + +1. Sub-header: "Per-scope overrides". +2. Body: "Each scope uses the main connection by default. Choose 'Custom' to give a scope its own settings." +3. Three sub-rows, one per scope: + - **Discovery** — title "Search & metadata", subtitle "GitHub API, search results, repo details." + - **Downloads** — title "Downloads", subtitle "APK and asset downloads." + - **Translation** — title "Translation", subtitle "DeepL, Microsoft, LibreTranslate calls." + - Each row has a trailing **2-segment chooser** `[ Use main ] [ Custom… ]` (review M2). + - "Custom…" expands `AnimatedVisibility` into a mini-editor matching Card 1 structure (mode segment + form + test + save). Test button label: **"Test for Downloads"** (review C2). Save here writes only to that scope. + - Closed (Use main selected) sub-rows show a compact status: green pill "Using main" or — if "Custom…" persists a non-default config but the user just toggled back to "Use main" — the saved override config is preserved in state but not applied. Toggle back to "Custom…" to restore. +4. Live state pill on every override sub-row (review C2 #4): + - "Use main" → small green-ish pill "Using main" (`tertiaryContainer`, `labelSmall`). + - "Custom…" with config → amber-ish pill "HTTP 127.0.0.1:1080" (`tertiaryContainer`, `labelSmall`, mono font). + +**Empty / loading / error states**: + +- **Empty**: never — "No proxy" mode is always available. +- **Loading test**: Test button shows inline `CircularProgressIndicator` (16.dp). Disable both Test and Save while testing. +- **Test success**: snackbar with 3-line breakdown (master) or 1-line scope-specific (override). +- **Test failure (one endpoint)**: snackbar "Search ✓ · Downloads ✗ connection refused · Translation ✓". Don't fail the whole test — show partial. +- **Test failure (all endpoints)**: snackbar with retry button. +- **Save success**: snackbar "Main connection saved." or "Downloads connection saved." +- **Save error**: snackbar with retry. +- **Mode switch with unsaved form draft**: switching the master mode from HTTP/HTTPS to SOCKS5 (or vice versa) with dirty form: confirmation snackbar "Switch mode? Unsaved settings will be cleared. [Switch] [Cancel]." (covers review C4 scope-changing pattern.) +- **Override on→off toggle ("Custom…" → "Use main")**: snackbar "Downloads now uses the main connection." Reversible — toggle back within 5s to restore the override config. + +**Direct vs No-proxy clarity** (review C3): "No proxy" replaces "Direct" / "None" everywhere. Body copy updated. `proxy_none_description` rewritten to "The app connects to the internet without a proxy." (See §6.) + +### 4.4 Validation rules (unchanged from V1) + +- Host: existing `isLikelyValidProxyHost(raw)`. Inline error in `supportingText`. +- Port: integer in `1..65535`. +- Username + password: optional. +- Save button disabled until host + port pass. +- Test button enabled even when fields invalid; surfaces friendly snackbar "Enter a host and port first." + +### 4.5 Master proxy persistence migration + +`ProxyRepository` schema additions: + +- New record key: `proxy_master` → `ProxyConfig?` (nullable for "no master configured"). +- New per-scope key: `proxy__use_master` → `Boolean` (default true). +- Existing per-scope `proxy_` records remain; semantics now mean "the scope's override config" (only consulted when `proxy__use_master == false`). + +**One-time migration on first launch of V2**: + +1. Read existing 3 per-scope records. +2. If all 3 are equal (same `ProxyConfig`): write that config to `proxy_master`, set all 3 `proxy__use_master = true`. +3. If any diverge: write the **most common** config to `proxy_master` (ties broken by scope order: Discovery > Downloads > Translation). For each scope whose config differs from the chosen master, set `proxy__use_master = false` (existing record stays as the override). +4. Bump DataStore version key from N → N+1 to gate the migration (`tweaksDataStore.version`). Run once. + +This is a pure-presentation migration — no network calls, no async beyond DataStore reads. Failures roll back (don't bump version key) so retry on next launch is safe. + +`TweaksAction` additions: + +- `OnMasterProxySave(config: ProxyConfig)` — writes master + all scopes' `useMaster = true`. +- `OnScopeUseMainToggled(scope: ProxyScope, useMain: Boolean)` — toggles override; preserves the scope's override config in state when toggling to main. +- `OnScopeProxySave(scope: ProxyScope, config: ProxyConfig)` — writes scope override + sets `useMaster = false`. +- Existing `OnProxyTest(scope: ProxyScope?)` — `null` scope = master (tests 3 endpoints). + +--- + +## 5. Visual primitive proposals + +### 5.1 New primitives + +**`TweaksEntryRow`** — described in §2.2. Lives at `feature/tweaks/presentation/components/TweaksEntryRow.kt`. + +**`ModePillSegment`** — promote existing `ModeSegment` from `sections/Others.kt` (private) into `core/presentation/components/ModePillSegment.kt`. Generic over a value type: + +``` +data class ModePillItem(val value: T, val label: String, val icon: ImageVector? = null, val caption: String? = null) +@Composable fun ModePillSegment(items: List>, selected: T, onSelect: (T) -> Unit, modifier: Modifier = Modifier) +``` + +Used by: +- Appearance theme mode (Light / Dark / Follow system). +- Connection master mode (No proxy / System / HTTP/HTTPS / SOCKS5) — with `caption` for the bottom-of-pill hint. +- Content width segment (Compact / Wide / Extra wide). + +**`UseMainSegment`** — 2-segment `[ Use main ] [ Custom… ]`. Lives at `feature/tweaks/presentation/components/UseMainSegment.kt`. Two `ToggleButton`-style chips, `Radii.chip` shape, primary fill on selected. (Could be implemented as a thin wrapper around `ModePillSegment` — implementer's call.) + +**`RestartBanner`** — `feature/tweaks/presentation/components/RestartBanner.kt`. Outlined `Radii.row` `tertiaryContainer`-tinted Surface. Props: `reasons: Set`, `onRestartNow: () -> Unit`, `onLater: () -> Unit`. + +**`CustomForgesSheet`** — `GhsBottomSheet` instance, no new shape primitive. New composable at `feature/tweaks/presentation/components/CustomForgesSheet.kt`. Replaces `CustomForgesDialog.kt`. + +**`PasteProxyUrlSheet`** — `GhsBottomSheet`. Single paste field + parser. Lives at `feature/tweaks/presentation/components/PasteProxyUrlSheet.kt`. + +**`LicensesScreen`** — `feature/tweaks/presentation/LicensesScreen.kt`. Static markdown view fed by `gradle-license-plugin` JSON committed to assets. Outlined `Radii.row` row per library, expandable for full license text. + +### 5.2 No new shapes + +All squircles reuse `Radii.row`, `Radii.chip`, `WonkySquircleShape.{CtaPrimary, CtaAlt, Search, Sheet, Dialog, Toast}`. + +### 5.3 Components to retire + +- `ExpressiveCard` usages inside Tweaks sections (`Others.kt`, `Installation.kt`) — replace with outlined `Radii.row` Surface. +- `ElevatedCard(shape = RoundedCornerShape(32.dp))` in `Translation.kt`, `Language.kt`, `About.kt`, `Network.kt` — same replacement. +- `RoundedCornerShape(12.dp)` `OutlinedTextField` inside forms — switch to `Radii.chip`. +- 3× `ProxyScopeCard` — deleted, replaced by §4 design. +- `CustomForgesDialog.kt` — deleted, replaced by `CustomForgesSheet`. + +--- + +## 6. String rename table + +Resource keys + English values. Across-13-locale translation queued via §9 CSV; English ships first per project policy. + +| Resource key | Current text | Proposed text | +|---|---|---| +| `Res.string.tweaks_title` | "Tweaks" | "Tweaks" (keep) | +| `Res.string.section_appearance` | "Appearance" | "Appearance" (keep) | +| `Res.string.theme_color` | "Theme color" | "Palette" | +| `Res.string.theme_light` | "Light" | "Light" (keep) | +| `Res.string.theme_dark` | "Dark" | "Dark" (keep) | +| `Res.string.theme_system` | "System" | "Follow system" | +| `Res.string.amoled_black_theme` | "AMOLED black" | "True black (AMOLED)" | +| `Res.string.amoled_black_description` | "Use pure black for OLED screens." | "Pure-black background — saves power on OLED screens." | +| `Res.string.system_font` | "System font" | "Use system font" | +| `Res.string.system_font_description` | "Use the system font for the app." | "Use your device's default typeface instead of Geist." | +| `Res.string.scrollbar_option_title` | "Show scrollbar" | "Show scrollbar" (keep) | +| `Res.string.scrollbar_option_description` | "Show scrollbar on the right side." | "Always show the scrollbar on long pages." | +| `Res.string.content_width_title` | "Content width" | "Content width" (keep) | +| `Res.string.content_width_description` | "Adjust max content width." | "How wide content should stretch on big windows." | +| `Res.string.section_language` | "Language" | "Language" (keep) | +| `Res.string.language_intro` | "Choose your app language." | "Pick the language used across the app." | +| `Res.string.language_picker_title` | "App language" | "App language" (keep) | +| `Res.string.language_picker_description` | "Restart required after change." | "The app restarts when you switch language." | +| `Res.string.language_follow_system` | "Follow system" | "Follow system" (keep) | +| `Res.string.language_follow_system_subtitle` | (new) | "Follow system · %1$s" (resolved tag interpolated) | +| `Res.string.section_network` | "Network" | (retired — block becomes "Connectivity" §1.1) | +| `Res.string.section_connectivity` | (new) | "Connectivity" | +| `Res.string.section_privacy_and_data` | (new) | "Privacy & data" | +| `Res.string.section_app_block` | (new) | "App" | +| `Res.string.section_installs_and_updates` | (new) | "Installs & updates" | +| `Res.string.connection_entry_title` | (new) | "Connection" | +| `Res.string.connection_entry_subtitle_no_proxy` | (new) | "No proxy" | +| `Res.string.connection_entry_subtitle_system` | (new) | "System proxy" | +| `Res.string.connection_entry_subtitle_proxy_with_overrides` | (new) | "%1$s · %2$d override" / "%1$s · %2$d overrides" (plural-aware) | +| `Res.string.sources_entry_title` | (new) | "Sources" | +| `Res.string.proxy_scope_intro` | "Configure proxies per scope." | (retired) | +| `Res.string.proxy_scope_discovery_title` | "Discovery proxy" | "Search & metadata" | +| `Res.string.proxy_scope_download_title` | "Download proxy" | "Downloads" | +| `Res.string.proxy_scope_translation_title` | "Translation proxy" | "Translation" | +| `Res.string.proxy_scope_discovery_description` | "Used for API and search." | "GitHub API, search results, repo details." | +| `Res.string.proxy_scope_download_description` | "Used for APK downloads." | "APK and asset downloads." | +| `Res.string.proxy_scope_translation_description` | "Used for translation providers." | "DeepL, Microsoft, LibreTranslate calls." | +| `Res.string.proxy_none` | "None" | "No proxy" | +| `Res.string.proxy_none_description` | "No proxy used." | "The app connects to the internet without a proxy." | +| `Res.string.proxy_system` | "System" | "System" (keep) | +| `Res.string.proxy_system_description` | "Use system proxy." | "Use the proxy configured in your OS." | +| `Res.string.proxy_http` | "HTTP" | "HTTP/HTTPS" | +| `Res.string.proxy_http_caption` | (new) | "Most corporate proxies." | +| `Res.string.proxy_socks` | "SOCKS" | "SOCKS5" | +| `Res.string.proxy_socks_caption` | (new) | "Tor, SSH tunnels." | +| `Res.string.proxy_test` | "Test" | (retired — replaced by scope-named buttons) | +| `Res.string.proxy_test_main` | (new) | "Test main connection" | +| `Res.string.proxy_test_scope` | (new) | "Test for %1$s" (scope name interpolated) | +| `Res.string.proxy_save` | "Save" | "Save" (keep) | +| `Res.string.proxy_test_success` | "Proxy reachable (%d ms)" | "Connected in %1$d ms" | +| `Res.string.proxy_test_main_success` | (new) | "Search ✓ %1$d ms · Downloads ✓ %2$d ms · Translation ✓ %3$d ms" | +| `Res.string.proxy_use_main` | (new) | "Use main" | +| `Res.string.proxy_use_custom` | (new) | "Custom…" | +| `Res.string.proxy_using_main_pill` | (new) | "Using main" | +| `Res.string.proxy_paste_full_url` | (new) | "Paste full URL" | +| `Res.string.proxy_paste_url_placeholder` | (new) | "scheme://user:pass@host:port" | +| `Res.string.proxy_paste_url_error` | (new) | "Couldn't read that URL." | +| `Res.string.connection_intro_title` | (new) | "How the app reaches the internet" | +| `Res.string.connection_intro_body` | (new) | "Pick a connection mode below. Most people leave this on No proxy." | +| `Res.string.connection_inline_helper` | (new) | "Applies to all traffic unless overridden below." | +| `Res.string.connection_overrides_body` | (new) | "Each scope uses the main connection by default. Choose 'Custom' to give a scope its own settings." | +| `Res.string.mirror_tweaks_entry_label` | "Mirror" | "GitHub mirror" | +| `Res.string.custom_forges_entry_label` | "Custom forges" | "Custom forges" (keep) | +| `Res.string.custom_forges_entry_subtitle` | "Add your own Forgejo/Gitea hosts." | "Add a Forgejo or Gitea host" | +| `Res.plurals.custom_forges_count` | (new) | "%d host" / "%d hosts" (plural-aware) | +| `Res.string.section_translation` | "Translation" | "Translation" (keep) | +| `Res.string.translation_intro` | "Configure translation provider and auto-translate." | "Pick the engine the app uses to translate READMEs." | +| `Res.string.translation_provider_title` | "Provider" | "Provider" (keep) | +| `Res.string.translation_provider_description` | "Pick a translation engine." | (retired) | +| `Res.string.translation_auto_title` | "Auto-translate" | "Translate READMEs automatically" | +| `Res.string.translation_auto_subtitle` | "Translate READMEs when opening repos." | "When opening a repo, translate the README into your target language." | +| `Res.string.section_installation` | "INSTALLATION" | "Install method" | +| `Res.string.install_method_android_only_badge` | (new) | "Android only" | +| `Res.string.install_method_desktop_empty_title` | (new) | "Install method is Android-only" | +| `Res.string.install_method_desktop_empty_body` | (new) | "Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager." | +| `Res.string.installer_type_default` | "Default" | "System installer" | +| `Res.string.installer_type_default_description` | "Uses Android's default installer." | "Asks each time. Works on every device." | +| `Res.string.installer_type_shizuku` | "Shizuku" | "Shizuku" (keep) | +| `Res.string.installer_type_shizuku_description` | "Silent install via Shizuku." | "Silent install. Needs Shizuku app running." | +| `Res.string.installer_type_dhizuku` | "Dhizuku" | "Dhizuku" (keep) | +| `Res.string.installer_type_dhizuku_description` | "Silent install via Dhizuku." | "Silent install. No root needed." | +| `Res.string.installer_type_root` | "Root" | "Root" (keep) | +| `Res.string.installer_type_root_description` | "Silent install with root." | "Silent install via root. Power-user only." | +| `Res.string.installer_attribution_title` | "Installer attribution" | "Pretend the installer is…" | +| `Res.string.installer_attribution_description` | "Some apps reject silent installs unless installer claims to be Google Play." | "Some apps reject silent installs unless the installer claims to be Google Play. This setting controls what we claim." | +| `Res.string.section_updates` | "UPDATES" | "Update behavior" | +| `Res.string.auto_update_title` | "Auto-update" | "Install updates in the background" | +| `Res.string.auto_update_description` | "Apply downloaded updates automatically." | "Apply downloaded updates without notifying you each time." | +| `Res.string.update_check_enabled_title` | "Background update check" | "Check automatically" | +| `Res.string.update_check_enabled_description` | "Periodically check for new releases." | "Look for new releases on a schedule." | +| `Res.string.update_check_interval_title` | "Check interval" | "Check every" | +| `Res.string.update_check_interval_description` | "How often to check for updates." | (retired) | +| `Res.string.update_interval_6h` | (new / rename) | "Every 6 hours" | +| `Res.string.update_interval_12h` | (new / rename) | "Every 12 hours" | +| `Res.string.update_interval_24h` | (new / rename) | "Daily" | +| `Res.string.update_interval_manual` | (new) | "Manual only" | +| `Res.string.update_desktop_check_on_launch_title` | (new) | "Check on launch" | +| `Res.string.update_desktop_check_on_launch_subtitle` | (new) | "Check for updates each time the app starts." | +| `Res.string.include_pre_releases_title` | "Include pre-releases" | "Include pre-releases" (keep) | +| `Res.string.include_pre_releases_description` | "Treat alpha/beta as available updates." | "Show alpha and beta tags as available updates." | +| `Res.string.skipped_updates_entry_title` | "Skipped updates" | "Skipped updates" (keep) | +| `Res.plurals.skipped_updates_count` | (new) | "%d app" / "%d apps" (plural-aware) | +| `Res.string.skipped_updates_empty_subtitle` | (new) | "Nothing skipped" | +| `Res.string.hidden_repositories_title` | "Hidden repositories" | "Hidden repositories" (keep) | +| `Res.plurals.hidden_repositories_count` | (new) | "%d repo" / "%d repos" (plural-aware) | +| `Res.string.hidden_repositories_empty_subtitle` | (new) | "No hidden repos" | +| `Res.string.storage` | "Storage" | "Storage" (keep — now narrower scope) | +| `Res.string.downloaded_packages` | "Downloaded packages" | "Downloaded APKs" | +| `Res.string.downloaded_packages_description` | "Installer files kept for resumed updates." | "We keep installers around so updates resume fast." | +| `Res.string.current_size` | "Current size:" | "Using:" | +| `Res.string.section_privacy_screen_title` | (new) | "Privacy" | +| `Res.string.privacy_entry_subtitle` | (new) | "Compact state (see §3.9 hub subtitle rules)" | +| `Res.string.privacy_usage_data_subheader` | (new) | "Usage data" | +| `Res.string.privacy_telemetry_title` | (new) | "Share anonymous usage data" | +| `Res.string.privacy_telemetry_subtitle` | (new) | "Help us understand which features get used." | +| `Res.string.privacy_telemetry_collect_expand` | (new) | "What we collect" | +| `Res.string.privacy_telemetry_collect_body` | (new) | "App version. OS and platform. Feature usage counts. No repo names. No tokens. No identifiers." | +| `Res.string.privacy_telemetry_on_snackbar` | (new) | "Sharing usage data starting next launch." | +| `Res.string.privacy_telemetry_off_snackbar` | (new) | "Usage data sharing stopped. Existing data is dropped." | +| `Res.string.auto_detect_clipboard_links` | "Auto-detect clipboard links" | "Detect repo links in clipboard" | +| `Res.string.auto_detect_clipboard_description` | "Detect copied repo URLs and offer to open them." | "When you copy a github.com or codeberg.org link, we'll prompt to open it." | +| `Res.string.privacy_history_subheader` | (new) | "Browsing history" | +| `Res.string.hide_seen_title` | "Hide seen" | "Hide repos I've already viewed" | +| `Res.string.hide_seen_description` | "Skip already-viewed repos in feeds." | "Skip seen repos in feeds and search." | +| `Res.string.clear_seen_history` | "Clear seen history" | "Clear viewed history" | +| `Res.string.clear_seen_history_description` | "Reset the seen-repo list." | "Forget which repos you've already opened." | +| `Res.string.clear_seen_history_confirm` | (new) | "Clear all viewed history? This won't unstar or unfavorite anything." | +| `Res.plurals.host_tokens_count` | (new) | "%d token" / "%d tokens" (plural-aware) | +| `Res.string.host_tokens_empty_subtitle` | (new) | "No tokens yet" | +| `Res.string.section_about` | "About" | "App info" (sub-screen title) | +| `Res.string.app_info_tagline` | (new) | "Cross-platform app store for GitHub, Codeberg, and Forgejo releases." | +| `Res.string.app_info_action_whats_new` | (new) | "What's new" | +| `Res.string.app_info_action_whats_new_subtitle` | (new) | "Past release notes." | +| `Res.string.app_info_action_licenses` | (new) | "Open source licenses" | +| `Res.string.app_info_action_licenses_subtitle` | (new) | "Libraries used in the app." | +| `Res.string.app_info_action_privacy_policy` | (new) | "Privacy policy" | +| `Res.string.app_info_action_privacy_policy_subtitle` | (new) | "View on github-store.org." | +| `Res.string.app_info_action_source_code` | (new) | "Source code on GitHub" | +| `Res.string.app_info_action_source_code_subtitle` | (new) | "View this app's source." | +| `Res.string.app_info_version_copied` | (new) | "Version copied." | +| `Res.string.version` | "Version" | "Version" (keep) | +| `Res.string.feedback_send` | "Send feedback" | "Send feedback" (keep) | +| `Res.string.feedback_hub_subtitle` | (new) | "We read every report." | +| `Res.string.tweaks_search_placeholder` | (new) | "Search settings" | +| `Res.string.tweaks_search_empty` | (new) | "No settings match '%1$s'." | +| `Res.string.restart_banner_body` | (new) | "Some changes need a restart to apply." | +| `Res.string.restart_banner_reasons_prefix` | (new) | "Affected: %1$s" (comma-joined reasons) | +| `Res.string.restart_banner_reason_language` | (new) | "language" | +| `Res.string.restart_banner_reason_theme` | (new) | "theme" | +| `Res.string.restart_banner_reason_telemetry` | (new) | "usage data" | +| `Res.string.restart_banner_restart_now` | (new) | "Restart now" | +| `Res.string.restart_banner_later` | (new) | "Later" | +| `Res.string.menubar_help_menu` | (new, desktop) | "Help" | +| `Res.string.menubar_help_about` | (new, desktop) | "About GitHub Store" | +| `Res.string.menubar_help_feedback` | (new, desktop) | "Send feedback…" | +| `Res.string.menubar_help_licenses` | (new, desktop) | "Open source licenses" | +| `Res.string.menubar_help_privacy` | (new, desktop) | "Privacy policy" | + +i18n constraints (review M6): + +- Every counted label uses `pluralStringResource` (Compose Multiplatform Resources, `org.jetbrains.compose.resources`). Plural categories per locale follow CLDR (Russian 3 forms, Polish 3, Arabic 6, English 2). +- Subtitle ~32 chars; if subtitle has multiple dot-separated tokens, drop rightmost on overflow. +- Locale-aware separator (`·` for Latin-script locales, ` / ` for CJK in narrow contexts) — pragmatic compromise: use `·` everywhere in v1 since the existing `Squiggle`-adjacent text already does, queue CJK separator audit for P13. +- Translation handoff CSV: §9. + +--- + +## 7. Risks / open questions / non-goals + +### 7.1 Migration concerns + +- **Deep links** — `githubstore://tweaks/` deferred to follow-up ticket P12.5.1 (review C5 partially adopted: inline search ships, deep links queued). Route name list in §2.5. +- **Screenshots in docs** — README + Play Store + Homebrew tap screenshots that show today's Tweaks layout will be stale. Action: list and regen after merge. +- **Discoverability loss** — mitigated by (a) dynamic hub subtitles, (b) inline search (§2.5), (c) badged cross-platform rows (review M8). +- **ProxyRepository schema bump** — see §4.5. Migration tested via "all 3 equal," "2 equal + 1 different," "all 3 different" + clean install. Implementer must add a unit test for the migration's plurality-vote rule. + +### 7.2 Implementation risks + +- **`HostTokensScreen` row vocabulary audit** — confirm `Radii.row` + full-opacity `outline` border. Refactor in P12.5 if not. +- **`MirrorPickerScreen` row vocabulary audit** — same. +- **`FeedbackBottomSheet`** — confirm `WonkySquircleShape.Sheet`. +- **String resource churn** — touches ~80 keys (V1 had ~50; V2 added telemetry, restart banner, paste URL, menubar, search, plurals). Across 13 locales = ~1040 string updates. Ship strategy per project policy: English first, translator handoff CSV (§9) queued. +- **Action sealed-interface drift** — `TweaksAction` will balloon. Recommendation (not blocking v1): split into `TweaksHubAction`, `TweaksConnectionAction`, `TweaksPrivacyAction`, etc., each handled by either the same VM with grouped `when` or per-sub-screen sub-VMs. Refactor pairs naturally with this redesign. +- **`gradle-license-plugin`** — verify it's configured; if not, add in this phase. Output JSON goes to `composeApp/src/commonMain/composeResources/files/licenses.json`. Parsed at runtime by `LicensesScreen`. +- **Border contrast audit gate** — before merge, screenshot all 4 palettes × 3 modes (Light/Dark/AMOLED) and visually confirm `TweaksEntryRow` border passes 3:1 contrast on Cream light. If `outline` reads too heavy on dark themes, fall back to `outline.copy(alpha = 0.55f)` but only after measured contrast > 3:1 on Cream light. + +### 7.3 v1 non-goals (explicit) + +- **PAC files / proxy auto-config URL.** Documented non-goal per review M3. May add as a 5th mode pill in P13 if user demand surfaces. Today's Mode segment is **No proxy / System / HTTP/HTTPS / SOCKS5**, 4 pills. +- **SOCKS4 / SOCKS4a.** `ProxyConfig.Socks` is SOCKS5 only. Mode pill labeled "SOCKS5" to set expectation. +- **Multiple proxy presets / proxy profiles.** Out of scope. +- **In-screen telemetry data export.** Out of scope. Privacy expandable only describes; doesn't export. +- **Cross-screen settings search (system-level).** v1 search is hub-only (§2.5). Filtering through every sub-screen's controls is P13+. + +### 7.4 UX research follow-ups (queued, not blocking v1) + +1. Hub feels sparse on tall display — accept; revisit if heatmap shows abandoned scrolls. +2. "Connection" vs "Network" naming — locked to "Connection" + "Sources" per IA. +3. Provider radios vs chips — locked to radios per §3.5. +4. Install method "System installer" naming — locked. +5. About card hero vs 3 rows — locked to single hero card. +6. "Use main" vs "Inherit" wording — locked to "Use main" / "Custom…" segment per review M2. + +### 7.5 Open items the architect can't resolve alone + +- **Telemetry backend.** Spec defines the UI for opt-out. Whether `TelemetryRepository` already has a backend wired (per `feature/tweaks/CLAUDE.md` it's injected but UI is absent today) needs product confirmation. If backend isn't ready, the toggle persists locally + no-ops until backend ships. Worst case, it's a UX-correct placebo for one release; review C1 still satisfied because the user-facing control exists. +- **Crash reporter desktop opt-out.** `CrashReporter` (desktop) writes local logs; spec doesn't add a UI toggle for it because it's local-only (no network exfil). If a future release adds remote crash upload, gate it on the same telemetry toggle. + +### 7.6 Things deliberately not changed + +- The `AppLanguages.ALL` list and tag-based persistence. +- The `ProxyConfig` sealed class (only `ProxyRepository` schema bumps — see §4.5). +- The `TweaksRepository` persisted prefs keys for non-renamed settings (`RestartReason` + `needsRestartReasons` is **new**). +- The `MirrorPickerScreen`, `SkippedUpdatesScreen`, `HiddenRepositoriesScreen`, `HostTokensScreen`, `WhatsNewHistoryScreen` routes and nav wiring. +- The `FeedbackBottomSheet` flow. +- Cross-cutting tokens (`Tokens.kt`, `Radii.kt`, `WonkySquircleShape.kt`). + +--- + +## 8. Desktop MenuBar spec (new — placement Option E) + +Today's `composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt` `Window { … }` has no `MenuBar` content. This is **additive** — no current menu is being overridden. + +**Spec**: + +``` +Window(...) { + MenuBar { + Menu("Help") { + Item("About GitHub Store", onClick = { /* navigate to App info */ }) + Item("Send feedback…", onClick = { /* open FeedbackBottomSheet */ }) + Item("Open source licenses", onClick = { /* navigate to App info → Licenses */ }) + Item("Privacy policy", onClick = { /* open github-store.org/privacy */ }) + } + } + // ... existing window content +} +``` + +**macOS specifics**: + +- The JVM injects a default "About GitHub-Store" item under the macOS app menu. Override the no-op default via `Desktop.getDesktop().setAboutHandler { /* navigate to App info */ }` in `DesktopApp.main`. The Apple-menu About item then routes to our in-app App info screen. +- The Help menu still renders on macOS (per JVM `MenuBar` semantics), giving feedback + licenses + privacy a discoverable home. + +**Windows/Linux specifics**: + +- `MenuBar` renders in the title-bar area. Help menu is the entry point. No system About menu to override. + +**i18n**: menu strings are added to `strings.xml` per §6 (`menubar_help_menu`, `menubar_help_about`, etc.). At runtime, the JVM `Menu` and `Item` labels are resolved via the same string catalog used by Compose UI. + +**Why this matters**: every long-tail desktop app — VS Code, Slack, Discord, 1Password — exposes About via the native menu bar. Today our Compose Multiplatform `Window` has no menu, so desktop users hunt for About in the in-app UI. Wiring this is ~25 lines + zero changes to mobile. + +--- + +## 9. Translator handoff CSV + +Generated as **`/Users/rainxchzed/Documents/development/kmp/GitHub-Store/.design/P12_5_TWEAKS_STRINGS.csv`** alongside this spec. Three-column shape: `Resource key | Old English | New English`. New keys list as old="(new)". + +CSV is the source of truth for the translator queue. English ships first per project policy; translations land in subsequent commits as translators turn each locale around. + +--- + +## 10. Implementation order suggestion + +Suggested wave-based parallelization for `gsd-execute-phase`: + +- **Wave 1** — Foundation + - Schema migration scaffolding (`ProxyRepository` master + per-scope `useMaster`, §4.5). + - `RestartReason` enum + `needsRestartReasons` in `TweaksRepository`. + - `RestartBanner` component (§5.1). + - `TweaksEntryRow` primitive (§2.2). + - New navigation routes (placeholders for unmade screens). + - `TweaksScreen` hub composable with restart banner + search field + section blocks (§2). + - 11 entry rows wired to existing or placeholder destinations. +- **Wave 2** — Quick-win sub-screens (parallel-safe; each just relocates existing controls) + - `TweaksAppearanceScreen` (§3.1) — easy, mostly relocation. + - `TweaksLanguageScreen` (§3.2) — searchable list pattern. + - `TweaksStorageScreen` (§3.8) — narrower than V1's Library cleanup; just downloaded APKs. + - `TweaksAppInfoScreen` (§3.11) — meta links + version card. + - `LicensesScreen` (new, §3.11 / §5.1) — requires `gradle-license-plugin` config. +- **Wave 3** — Connection redesign (biggest piece) + - `TweaksConnectionScreen` (§3.3 + §4). + - `ModePillSegment` promotion (§5.1). + - `UseMainSegment` (§5.1). + - `PasteProxyUrlSheet` (§5.1). + - VM action additions (§4.5). + - Schema migration unit tests. +- **Wave 4** — Privacy + Updates + Sources + Translation + Install (parallel-safe) + - `TweaksPrivacyScreen` (§3.9) — telemetry opt-out is highest-priority new control. + - `TweaksUpdatesScreen` (§3.7) — drops 3h interval. + - `TweaksSourcesScreen` (§3.4) + `CustomForgesSheet` (§5.1). + - `TweaksTranslationScreen` (§3.5) — provider radio redesign. + - `TweaksInstallScreen` (§3.6) + Android-only badge handling for desktop. +- **Wave 5** — Polish & cleanup + - Send feedback hub row wires to `FeedbackBottomSheet` directly (§3.12). + - String rename diff (English; CSV handoff for translators §9). + - Delete old `sections/Appearance.kt`, `sections/Language.kt`, `sections/Network.kt`, `sections/Translation.kt`, `sections/Installation.kt`, `sections/Others.kt`, `sections/About.kt`, `sections/SettingsSection.kt`, `components/CustomForgesDialog.kt`. + - Border-contrast audit gate (Nord/Cream/Forest/Plum × Light/Dark/AMOLED screenshots). +- **Wave 6** — Desktop additive + - `MenuBar` in `DesktopApp.kt` (§8). + - macOS `Desktop.setAboutHandler` wiring. + +End of spec. diff --git a/.design/P12_5_TWEAKS_RESEARCH_REVIEW.md b/.design/P12_5_TWEAKS_RESEARCH_REVIEW.md new file mode 100644 index 000000000..33d122298 --- /dev/null +++ b/.design/P12_5_TWEAKS_RESEARCH_REVIEW.md @@ -0,0 +1,394 @@ +# Tweaks Redesign — UX Research Review + +> Reviewer: UX-Research. Target: `.design/P12_5_TWEAKS_REDESIGN.md` by ArchitectUX. Date: 2026-05-23. +> Evidence base: Nielsen Norman Group (NN/g), Material 3 guidance, Apple HIG (Settings + Localization), WCAG 2.2, and direct comparison with Tailscale, Cloudflare WARP, Bitwarden, 1Password, iOS Settings, and Termux. Code references cite the architect's own line numbers in the spec. + +--- + +## Verdict + +**Iterate before ship.** The IA refactor is directionally right and most per-screen specs are sound, but the Connection redesign has a load-bearing mental-model gap, telemetry is silently missing, and several discoverability/accessibility/i18n risks need answers in writing — not in a comment thread during implementation. + +--- + +## Critical issues (must fix before implementation) + +### C1. Telemetry is not in the spec at all — but it's a first-class privacy control + +**Problem.** `TweaksViewModel` injects `TelemetryRepository` (confirmed in `feature/tweaks/CLAUDE.md` → "TweaksViewModel injects: …, TelemetryRepository, …"). The redesign spec contains **zero** occurrences of the word "telemetry" across all 668 lines. It is not on the hub, not in About, not in Library cleanup. If it exists today, the spec deletes it by omission. If it doesn't yet have UI, this is the redesign moment to add it. + +**Evidence.** Modern privacy-forward apps universally surface telemetry as an explicit user-facing toggle within one tap of Settings root: Bitwarden ("Help us improve" → off by default), Tailscale ("Send usage analytics"), Cloudflare WARP ("Diagnostics"), 1Password ("Help us improve 1Password"). Hiding a telemetry control inside a sub-screen, or worse omitting it, fails GDPR's "easy to find" requirement for consent management (Article 7(3)) and Apple App Store guideline 5.1.2. + +**Proposed fix.** Add a "Privacy" card to the **About** sub-screen (preferred — keeps "App"/About as the meta home) OR move it into **Library cleanup** and rename that screen "Privacy & cleanup" (the architect even hinted at this in §7.2 #6: *"a quieter alternative is 'Privacy & storage'"*). Surface: toggle "Share anonymous usage data" + 1-line body + link "What we collect →" (modal or expandable). Also: any crash-reporter opt-out, if `CrashReporter` (desktop) has one. + +**Severity.** Critical (legal/compliance + user trust + an injected repository with no UI is a code smell on its own). + +--- + +### C2. Connection screen — "master" is fictional and the user has no way to know which scope they're testing + +**Problem.** §4.2 #4 admits the master model is presentation-only: *"`ProxyRepository` stores per-scope `ProxyConfig` already. On open, we read all three; if they're identical, we treat them as 'master.' If they diverge, we mark the divergent ones as 'overridden'."* This derivation is invisible to the user. + +The bigger issue is in §4.3, Card 1 step 4: *"`Test` … runs `OnProxyTest` against `ProxyScope.DOWNLOAD` (canonical scope for testing the master)."* The user reads "Test" and thinks "test this proxy." The app actually tests Download scope only. If Discovery has an override and Download doesn't, master-test passes and the user assumes their setup is fine — but Discovery is broken. + +**Evidence.** +- NN/g, "Visibility of System Status" (Heuristic #1): system state must be reflected accurately. Hidden "canonical scope" violates this. +- Cloudflare WARP, Tailscale, and Bitwarden all expose a connection test that explicitly says *what* it's testing ("Test connection to coordination server", "Pinging derp1.tailscale.com…"). Anonymous "Test" with hidden semantics is the worst of both worlds. +- The "load semantics" itself is brittle: any time a user toggles an override on then off, repository writes diverge then converge across millisecond-asynchronous DataStore writes — Jetpack DataStore doesn't guarantee snapshot-atomic reads across three keys. Compare-for-equality risks racy "is this master?" decisions. + +**Proposed fix.** +1. **Persist the master concept as its own ProxyConfig record** in `ProxyRepository`, plus a `useMaster: Boolean` per scope. Don't derive it from equality. (Yes, this is a schema change — that's the cost of the chosen IA. §7.4 lists "ProxyRepository schema" as untouched; that decision should be reversed.) +2. **Test must announce the scope.** Card 1 test button label: "Test main connection". Per-scope override test: "Test for Downloads". Snackbar should always include scope name: "Downloads proxy connected in 184 ms." +3. **When master is selected, run the test against all 3 endpoints** (search/metadata GitHub API, download CDN, configured translation provider) and show a 3-line result. This is what Tailscale's `tailscale netcheck` does. Roundtrip cost is acceptable — testing is rare. +4. Surface "current state" pill on every override sub-row even when collapsed: green "Using main", or amber "Override: HTTP 127.0.0.1:1080". + +**Severity.** Critical (load-bearing UX claim + persistence correctness). + +--- + +### C3. "Direct" is the wrong rename and will mistranslate + +**Problem.** §6 row: `proxy_none` "None" → "Direct". The architect's reasoning (§4.3 *"'Direct' replaces today's `ProxyType.NONE`. Renamed for clarity — 'None' sounds like an error state."*) is half-right and half-wrong. + +**Evidence.** +- "Direct" is industry term in *English* (Firefox, Chrome, system proxy.pac syntax: `DIRECT`). In the 13 locales the app ships, **"Direct" has no shared meaning**. In German "Direkt" works; in Chinese (Simplified) the literal "直接" is awkward UI copy — Chrome zh-CN uses "不使用代理服务器" ("do not use proxy server"); in Russian "Прямое соединение" is verbose. In Polish "Bezpośrednio" — same problem. +- The original "None" maps cleanly across locales because every translator already has localized "None" from the rest of the OS. +- iOS HIG, Internationalization: *"Avoid technical jargon and idioms that may not translate. Prefer concrete nouns over abstract states."* +- "Direct" also collides with the Connection screen body copy: *"Pick a connection mode below. Most people leave this on Direct."* — a user reading that translation might wonder "direct what?" + +**Proposed fix.** Three options, ranked: +1. **"No proxy"** — concrete, translatable, accurate. Used by Firefox UI ("No proxy"). Bitwarden uses this exact phrase. +2. **"Off"** — pairs naturally with a master pill ("Connection: Off"). Cloudflare WARP uses "Off / On / Auto". +3. Keep "Direct" but mark the rename as English-only and require translator notes for each locale ("the connection bypasses any proxy"). Worst option. + +Pick #1. Also rename `proxy_none_description` from "Connect directly, no proxy." to "The app connects to the internet without a proxy." + +**Severity.** Critical (13 locales × one mistranslation per locale = compounding bad copy). + +--- + +### C4. Empty-state and confirmation patterns are spec'd inconsistently across sub-screens + +**Problem.** The spec handles empty states on some screens but ignores them on others: +- §3.4 Sources: explicit empty state for custom forges ✔. +- §3.7 Updates: skipped/hidden drill rows show counts but **no spec for "0 items"** subtitle copy. Today's `skipped_updates_entry_description` is just descriptive; with the new dynamic-subtitle pattern (§2.2 *"reflects the current value of that domain"*), a zero state needs friendly copy. +- §3.9 Access tokens: *"No visual change needed beyond making sure its rows already use `Radii.row`"* — but the hub subtitle (§1.1) says "No tokens yet" when empty. The actual `HostTokensScreen` empty state is unaudited. A user tapping "No tokens yet" deserves an "Add your first token" CTA, not a blank list. +- §3.8 Library cleanup: cache size "Using: 124 MB" but no spec for "Using: 0 B" copy or whether the Clear button should be disabled. + +Confirmation dialogs are also inconsistent: +- §3.8 Clear viewed history: uses `GhsConfirmDialog`. Good. +- §3.4 Remove custom forge: uses `GhsConfirmDialog`. Good. +- §4 Connection override toggle-off: **no confirmation**, but toggling off override loses test results and arguably changes behavior across an entire scope. Should at least surface a snackbar "Discovery now uses main connection" so the destructive change is observable. +- §3.8 Clear downloaded APKs: uses existing `ClearDownloadsDialog`. Need to confirm copy is consistent with `GhsConfirmDialog` vocabulary post-D4 squircle scope. + +**Evidence.** NN/g, "10 Usability Heuristics," #5 Error prevention and #9 Help users recognize, diagnose, recover. Material 3 Confirmation Dialog guidance: *"Use for destructive actions that can't be undone, or that change scope of effect."* + +**Proposed fix.** Add an "Empty / loading / error states" sub-section to **every** per-screen spec. Standardize the destructive-action pattern across the redesign: +- Reversible state changes → snackbar with Undo (Material 3 standard). +- Irreversible destructive (clear cache, clear history, remove token, remove forge) → `GhsConfirmDialog`. +- Mode switches that change behavior across many scopes (proxy override on/off) → snackbar confirming scope change. + +**Severity.** Critical (inconsistency between sibling screens degrades trust + lookahead for translators). + +--- + +### C5. Hub navigation cost is real, and the spec rejects all standard mitigations without testing them + +**Problem.** §2.6 rejects search; §1.2 declines greyed-out cross-platform rows; §7.1 (Discoverability loss) acknowledges *"Today a user scrolling Tweaks accidentally discovers 'Custom forges' or 'Hide seen.' In a hub-and-spoke model, those settings are one tap further away. Mitigation: the hub's dynamic subtitles … make the state visible without entering."* The mitigation is partial — subtitle visibility helps recognition, not initial discovery, and only for users who already know what to look for. + +**Evidence.** +- NN/g, "Site Map vs Search" (2019): site maps (= our hub) work for users who know domain vocabulary; search wins for users who know what they want but not where it lives. Power users (our audience) hover at both ends — proxy tinkerers know exactly what they want, but onboarding users browse. +- iOS Settings: ~30 hub rows but ships a search bar at top of every Settings screen. Apple HIG, Settings: *"Provide a search bar when your settings hierarchy is more than one level deep."* +- 1Password and Bitwarden Settings: both ship search. +- Material 3 Settings pattern (m3.material.io): "For more than 6 categories, provide search." We have 10. + +The architect's three reasons for rejecting search (§2.6) are: +1. *"A user can scan them in under 2 seconds."* True for first visit, false for return visits to a specific item. +2. *"contradicting the 'much cleaner' directive."* A search bar one row tall doesn't violate "much cleaner." +3. *"deep links … better than fuzzy search field."* These solve different problems — deep links from outside the app, search from inside. + +**Proposed fix.** Either: +1. **Ship a tiny search field** at the top of the hub (under the title, above the first block header), `WonkySquircleShape.Search` shape, filters rows + their subtitle text. Cost: ~20 lines of Compose. Removes the entire discoverability complaint. +2. **Or commit to deep links in v1, not P13** (§7.1 defers them). At minimum: `githubstore://tweaks/connection`, `githubstore://tweaks/translation`, `githubstore://tweaks/updates`. These can be invoked from notifications ("Battery optimization is blocking updates →"), from in-app banners (the existing `D3` themes-refreshed banner), and from external "open settings" deep links. + +Even better: ship both. Search is cheap, deep links are infrastructure. + +**Severity.** Critical (user-explicit "rethought" goal vs new friction; user is a power user who navigates Tweaks weekly, not yearly). + +--- + +## Major concerns (strongly recommend fixing) + +### M1. "Library cleanup" is two unrelated mental models stapled together + +**Problem.** The user's brief explicitly asks this question. §3.8 puts under one roof: +- Downloaded APK cache (disk-space concern, performance-y). +- Detect repo links in clipboard (privacy / convenience pref). +- Hide seen / Clear viewed history (privacy / behavior pref). + +These have nothing in common except "things the app remembers." The clipboard toggle in particular has **zero** to do with "cleanup." + +**Evidence.** +- NN/g, "Card Sorting": users group by goal, not by implementation. A user trying to recover disk space won't think to look here for clipboard prefs; a user trying to stop the app from peeking at clipboard won't think to look in a screen called "cleanup." +- iOS Settings splits these: General → iPhone Storage (disk), General → AirDrop & Handoff → Universal Clipboard (clipboard), Safari → Privacy (browsing history). + +**Proposed fix.** Two options: +1. **Split into "Storage" + "Privacy"** at the hub level. Storage = downloaded APKs only. Privacy = clipboard, hide seen, viewed history, **and the telemetry toggle from C1**. Adds one hub row (now 11), but groups by user goal. This is the architecturally honest answer. +2. **Keep one screen but rename to "Privacy & data"** and add internal sub-sections "Disk" / "Clipboard" / "History" with `titleSmall` sub-headers (the screen-internal convention from §3 general). Less disruptive, slightly muddier. + +Prefer #1. The user's "rethink each category" directive supports it. + +**Severity.** Major (named in user's original brief). + +--- + +### M2. Per-scope override toggle wording is wrong direction + +**Problem.** §4.3 Card 2 #3: *"Each row has a trailing toggle 'Use main connection' (default ON). When toggled OFF, the row expands … into a mini-editor."* §7.2 #3 already flags this: *"Could also be 'Inherit from main' — more technical, possibly clearer to power users."* + +The user audience is power users. "Use main connection" reads ambiguously — it sounds like enabling a connection, not inheriting a setting. The toggle is also semantically inverted from the visual: ON = no editor visible (you can't change anything). Users will toggle it to "see the controls" and then panic when they realize they just unhooked the override. + +**Evidence.** +- iOS Settings → Wi-Fi → Network → "Configure DNS": uses radio buttons "Automatic / Manual" — same problem, better solution. Two explicit choices, no toggle inversion. +- Tailscale → Network → Exit Node: uses "Inherit from network" vs explicit "Override". +- Material 3 Switch guidance: *"Use switches for on/off states of a single setting, not for revealing more options."* + +**Proposed fix.** Replace the toggle with a 2-way segment per scope row: +``` +[ Use main ] [ Custom… ] +``` +"Custom…" reveals the mini-editor. "Use main" hides it. This is what 1Password does for per-vault settings ("Inherit / Custom"), and Bitwarden for organization policies ("Use default / Override"). + +Bonus: a segment makes the "override is on" state visually obvious without subtitle parsing. + +**Severity.** Major (every power user hits this). + +--- + +### M3. PAC files / SOCKS variants / proxy URL paste are unaddressed + +**Problem.** §4.3 mode segment offers Direct / System / HTTP / SOCKS. The user community asking for proxy support (China + privacy-conscious EU + Tor) will request: +- **SOCKS4 vs SOCKS5** distinction. Today's `ProxyConfig.Socks` doesn't expose version — fine if SOCKS5 only, but worth confirming. Tor's local proxy is SOCKS5; some corporate proxies are SOCKS4a. +- **HTTPS proxy** vs HTTP proxy. Many corporate MITM proxies require HTTPS. Today's `ProxyConfig.Http` is ambiguous in name. +- **PAC files / proxy auto-config URL**. Mentioned in the review brief; not covered by the spec at all. Many enterprise users have a PAC URL from IT. Firefox + Chrome + macOS System Settings + Windows Internet Options all support PAC. +- **Paste full proxy URL** as a fast-path (e.g. `socks5://user:pass@127.0.0.1:1080`). Tailscale + warp-cli + curl all accept this format. Five fields is a lot of typing; one paste field is mass-market UX. + +**Evidence.** +- Tor Browser Bundle, Settings → Connection → "Use a bridge" → bridge string is one paste field, not 5 separate inputs. +- Cloudflare WARP-CLI supports `warp-cli set-custom-proxy` with full URL. +- Material 3 form guidance: *"If a single input can replace multiple, prefer the single input with smart parsing."* + +**Proposed fix.** +1. Confirm `ProxyConfig.Socks` is SOCKS5-only (spec it explicitly: rename mode pill "SOCKS5" rather than "SOCKS"). If SOCKS4 is needed, add a sub-toggle. +2. Confirm "HTTP" actually supports HTTPS proxies (it likely does via JVM `Proxy.Type.HTTP` which serves both). If yes, rename the pill "HTTP/HTTPS" or just leave "HTTP" with a tooltip. +3. Add a "Paste proxy URL" affordance: a small text-button below the form fields ("Paste full URL"). On click, opens a modal with one paste field that parses `scheme://user:pass@host:port` and populates the form. Don't replace the form; supplement it. +4. **PAC file**: spec it as an explicit non-goal for v1 with a one-line follow-up ticket, or commit to a Mode pill 5: "PAC URL". Don't leave it ambiguous — users will ask. + +**Severity.** Major (will generate GitHub issues within a week of release if not addressed). + +--- + +### M4. Color contrast on `outlineVariant.copy(alpha = 0.55f)` will fail WCAG AA on Cream (amber light) palette + +**Problem.** §2.2: *"`border = BorderStroke(1.dp, outlineVariant.copy(alpha = 0.55f))`"*. This is the universal border for the entire redesign. Material 3's `outlineVariant` is already low-contrast (~3:1 against `surfaceContainerLow`), and multiplying by 0.55 alpha makes it ~1.7:1 against light Cream surfaces. + +**Evidence.** +- WCAG 2.2, SC 1.4.11 Non-text Contrast: **3:1 minimum** for UI component boundaries. +- Material 3 contrast tables: `outlineVariant` is designed for low-emphasis dividers, not standalone container borders. When used as a container border (your case), Material recommends `outline` (which is `outlineVariant` boosted ~2x). +- The amber Cream light palette is on file in `D3`. Cream's `surfaceContainerLow` is high-luminance off-white; the border will be nearly invisible. + +**Proposed fix.** Either: +1. Drop the 0.55 alpha. Use `outlineVariant` at full opacity. +2. Use `outline` at 0.55 alpha (lands at roughly the same visual weight on dark themes but stays above 3:1 on Cream light). +3. Audit per-palette. The architect should screenshot Nord/Cream/Forest/Plum × Light/Dark/AMOLED with a candidate `TweaksEntryRow` and confirm border visibility before committing. + +Run option #2, then audit. + +**Severity.** Major (accessibility compliance + Cream light is the maintainer's recently-changed lead-theme; visually buggy there is highly visible). + +--- + +### M5. 48dp tap-target accounting + chevron a11y are unaudited + +**Problem.** §2.2 hub row: *"padding(horizontal = 16.dp, vertical = 14.dp)"* + icon tile 40.dp + two-line text. Total row height depends on font metrics; with Geist `titleMedium` (~22sp line height) + 2dp gap + `bodySmall` (~16sp) + 28dp = roughly **68dp tall**. ✔ exceeds 48dp. + +But: §3.4 Custom forges row has a trailing `IconButton` for delete ("Trailing: `Icons.Outlined.DeleteOutline` `IconButton` → confirmation"). A 24dp icon inside the row needs **48dp tap target** per Material guidance and **44pt** per Apple HIG. Spec doesn't say. Also: an `IconButton` *inside* a clickable row creates a nested-click trap — tapping near the icon may register the row click first (Compose ripple bubbles to outer clickable unless the inner is wrapped in `onClick = {}` with `interactionSource`). + +**Evidence.** +- Material 3 Touch Targets: 48 × 48dp minimum for all interactive elements. +- WCAG 2.2 SC 2.5.8 (AA): 24 × 24 CSS pixel minimum; 44 × 44 strongly recommended. +- Compose nested clickables: the outer `Modifier.clickable` swallows child clicks if not explicitly excluded via `consumeWindowInsets` or empty inner click handler (this has been a recurring bug — see Compose 1.6 release notes on `clickable` consumption). + +**Proposed fix.** +1. Spec the delete `IconButton` as 48dp min with internal 24dp icon and `Modifier.clip(Radii.chip)` for ripple. +2. Spec the parent row's `Modifier.clickable` to **not** be the only click handler — the row click navigates, but the delete button stops propagation. +3. A11y semantics for every hub row: + ``` + semantics { + role = Role.Button + contentDescription = "$title. $subtitle. Double-tap to open." + } + ``` + Chevron icon: `contentDescription = null` (decorative). Status pills (Install method "Needs permission"): exposed as a `liveRegion = Polite` so TalkBack announces re-evaluation when permissions change. +4. Override toggle accessibility (§4.3 Card 2): when toggled OFF and editor expands, announce "Discovery override on, custom proxy settings for Discovery." + +**Severity.** Major (accessibility is global app concern, not per-feature). + +--- + +### M6. i18n: hub-row subtitle truncation + plural rules + RTL are unspec'd + +**Problem.** §2.2: *"Subtitle: bodySmall, onSurfaceVariant, max 1 line, ellipsize."* §1.1 examples: *"DeepL · auto on"*, *"Auto · every 6h"*, *"Nord · Dark"*. These are English-shape sentences. + +- **German**: "Auto · alle 6 Stunden" (no abbreviation for "hour"; "alle 6 Std." possible but unusual). "Befolge Systemeinstellungen · Dunkel" for "Follow system · Dark" — 35 chars before "Dark" even appears. Subtitle ellipsizes at ~28 chars on mobile. +- **Russian**: "Каждые 6 часов" + provider names like "Майкрософт" — Cyrillic is wider per char. +- **CJK** (Simplified Chinese, Traditional Chinese, Japanese, Korean): the · separator may not parse — CJK uses 「、」 or 「·」 (different code point). Compose `Text` handles BOM but the dot character should be locale-aware. Also no spaces between words is the norm in CJK, so "DeepL · 自动开启" works but feels foreign. +- **RTL** (Arabic, Hebrew): the · separator order flips, chevron flips. App must wrap subtitles in `LocaleTextDirection.Content` so dot-separated tokens lay out right. +- **Plurals**: "%d host" / "%d hosts" (§6 row for `custom_forges_count_label`) — this requires Android plural resources (`plurals.xml`) per locale; Compose Multiplatform Resources supports plurals (`pluralStringResource` in `org.jetbrains.compose.resources` since 1.6). Russian has 3 plural forms, Polish has 3, Arabic has 6. Naive "%d hosts" breaks all of them. + +**Evidence.** +- W3C i18n best practices, "Don't use 'one' / 'other'" — use CLDR plural categories. +- Apple HIG Localization: *"Allow at least 50% extra width for German and Russian. CJK can compress 30%."* +- Compose Multiplatform Resources release notes: `pluralStringResource` is the supported API; today's code uses `stringResource(..., count)` which is **not** plural-aware. + +**Proposed fix.** +1. Mandate `pluralStringResource` for any subtitle / label with a count, and add plural XML for every locale that doesn't have it. Audit: %d tokens, %d hosts, %d apps (skipped updates count). +2. Spec subtitle truncation behavior: *"Truncate with ellipsis. If subtitle has multiple dot-separated tokens, prefer truncating the rightmost token first."* (This is hard in Compose; pragmatic compromise: cap subtitle at ~32 chars in any locale, drop secondary token if over.) +3. Use a locale-aware separator: `LocalConfiguration.current.locales` → CJK locales use 「· 」(with width adjustment) or " / "; non-CJK uses " · ". +4. Wrap all hub rows in `CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr)` only for the chevron icon — text content respects user direction. +5. **Translator handoff**: the §6 rename table is English-only. Generate a CSV of "old key → new English → context note" for translators per locale. Don't ship the redesign with stale 12 locales (the user's policy elsewhere ships with English first and queues translations; that's acceptable, but it must be explicit in the spec, not implicit). + +**Severity.** Major (13 locales is a load-bearing project property). + +--- + +### M7. About sub-screen swallows the emergency "Send feedback" path + +**Problem.** §1.3 / §3.10: Send feedback moves into About sub-screen. The hub no longer surfaces feedback. Today, a user encountering a bug can reach Feedback in: Profile → Tweaks → scroll to about → Feedback button. New flow: Profile → Tweaks → About row → Feedback row → sheet. **One extra tap** at the worst possible moment — the user is already frustrated. + +**Evidence.** +- NN/g, "Error recovery": friction during an error state is multiplied by user frustration. Each extra tap during a bug-report flow doubles the abandonment rate (Sauro 2016 on error-recovery UI). +- Most apps that have "Send feedback" or "Report a problem" surface it on the Settings root, not buried. Bitwarden: Settings → "Help" (one tap). 1Password: Settings → "Help" (one tap). Tailscale: Settings → "Bug report" (one tap). +- The user's brief: *"What about emergencies (something broke, user wants to report fast)?"* — direct callout. + +**Proposed fix.** Three patterns to consider: +1. **Keep feedback as its own hub row** at the bottom of the "App" block, alongside About. Cost: 11 rows in hub instead of 10. Worth it. +2. Add a **floating "Help" overflow icon** in the hub topbar (architect's §2.1 says no trailing — reconsider). Pulls open a sheet with "Send feedback / What's new / Privacy". +3. Surface feedback as a **persistent banner** at the bottom of the hub only when a crash has occurred in the current session (use `CrashReporter`'s presence). Best-case UX, more engineering. + +Ship #1. It's the cheapest and matches industry norm. + +**Severity.** Major (named in user's brief; emotional moment). + +--- + +### M8. Cross-platform conditional rules in §1.2 hide too much + +**Problem.** §1.2: *"Hub omits the Install method row entirely on desktop. No greyed-out placeholder."* §3.6 Desktop note: *"if reached via deep link on desktop, show a centered empty state — 'Install method is Android-only'"*. Hiding entirely is consistent with iOS HIG ("don't show what doesn't apply"). But: + +- Users supporting both platforms (e.g. running desktop + Android Pixel) will assume parity. Hiding means they can't even tell the feature exists. This contradicts the broader trend in this app: the Tweaks screen exists *because* both platforms share most settings. +- The architect's chosen mitigation (centered empty state on deep link) is asymmetric — works in one direction (Android user opens deep link on desktop) but not the other (desktop user wondering "how do I make APK install silent on my Pixel from here"). + +**Evidence.** +- Apple HIG Settings, Cross-platform: *"Where settings are platform-specific, surface a small badge instead of hiding entirely so users know the feature exists elsewhere."* +- Discord, Slack, 1Password all do this with "Coming soon on this platform" or "Mobile only" subtitle badges. + +**Proposed fix.** Show the row on desktop with a **subtitle badge** "Android only" (use `tertiaryContainer` pill), clicking shows the §3.6 empty state with explanation. Same for any future iOS-only or Linux-only rows. The visual cost (one row) is dominated by the discoverability gain. + +(Alternative: leave architect's decision intact, but require a one-line entry under About on desktop: "On Android: install method, silent updates, attribution.") + +**Severity.** Major (user is dual-platform power user). + +--- + +### M9. "Connection" + "Sources" hub block name "Network & data" doesn't match its rows + +**Problem.** §1.1 block "Network & data" contains: Connection, Sources, Translation, Access tokens. +- "Translation" is not network-related from the user's mental model — it's a feature of repo Details. A user looking for "Translation settings" won't think "Network & data." +- "Access tokens" is closer to "Account / Security," not "Network." + +**Evidence.** +- NN/g, "Information Scent": users follow labels that match their intent. "Translation" under "Network & data" has weak scent. +- iOS Settings groups Translation under "Apps" or "General"; Tokens / passwords are always under "Passwords & Security." +- The architect's own §6 rename: `section_network` "Network" → "Network & data" (hub block header) — calling this a "data" block is a stretch when 2 of 4 children are arguably not about data movement. + +**Proposed fix.** Re-block: +1. **Look & feel**: Appearance, Language ✔ (no change) +2. **Connectivity**: Connection, Sources ← new tighter block +3. **Content & translation**: Translation (single row; or merge with another sibling) ← OR put Translation under Look & feel since it's per-locale-y +4. **Security & accounts**: Access tokens, telemetry (from C1) ← new block +5. **Installs & updates** ✔ (no change) +6. **Storage** + **Privacy** (from M1) +7. **App**: About, Send feedback (from M7) + +That's 7 blocks for 12 rows — heavier than the current 4 blocks for 10 rows, but each block is internally coherent. Alternative compromise: keep 4 blocks but rename "Network & data" → "Connectivity" and move Translation out (e.g. into Look & feel or its own micro-block). + +**Severity.** Major (the user's brief explicitly asked about this). + +--- + +### M10. Restart-on-language-change UX preserved but not specified for the new flow + +**Problem.** §3.2: *"Validation / events: selecting a language fires `OnAppLanguageSelected(tag)` → existing `OnAppLanguageChangeRequiresRestart` snackbar with 'Restart now' action. Unchanged."* + +But the new flow is: hub → tap "Language" row → arrive on `TweaksLanguageScreen` → tap a language → snackbar appears on … which screen? If the snackbar appears on `TweaksLanguageScreen` and the user taps "Restart now," the app restarts and lands on Home (existing behavior) — user loses position. If the user dismisses the snackbar and navigates back to the hub, the snackbar host is gone — but the language is still set, and the app hasn't restarted. Are language and UI now in inconsistent state until the next restart? + +**Evidence.** +- Today's monolithic `TweaksRoot.kt` has one `SnackbarHostState` for the whole screen; the snackbar persists across user actions until dismissed. +- New per-sub-screen pattern (§3 general: *"each sub-screen has its own `SnackbarHostState`"*) means the language snackbar belongs to `TweaksLanguageScreen` only. Navigating back drops it. +- Material 3 Snackbar guidance: "Restart now" is a high-stakes action — should be a banner, not a transient snackbar, when stakes are this high. + +**Proposed fix.** +1. Replace the "Restart now" snackbar with a **persistent banner** at the top of `TweaksLanguageScreen` that says "Language changed. Restart the app to see it everywhere. [Restart now] [Later]". Reuse existing `Banner` if any, otherwise a `Radii.row` outlined Surface with `tertiaryContainer` tint. +2. Persist the "needs restart" flag in `TweaksRepository` so navigating back to the hub still shows a top-level banner: "Restart pending — some screens may still show the old language." +3. On hub, show the banner above the first block. On any sub-screen, show it at the top of the screen. Dismisses only on app restart. + +**Severity.** Major (today's behavior is acceptable monolithic; new IA breaks it implicitly). + +--- + +## Nits / polish + +- **N1.** §2.2 spring scale 0.98× — fine on mobile, may feel "stuck" on desktop with a mouse cursor. Recommend `0.985×` on desktop, `0.97×` on Android (per D10's mention of `MediumBouncy`). +- **N2.** §3.5 Auto-translate target picker "drill-row" → "smaller variant of the §3.2 Language screen" — should explicitly say "uses the same searchable list but filters to `SupportedTranslationLanguages.all`." Otherwise risk of two diverging language pickers maintained separately. +- **N3.** §3.7 "every 6h" pill labels: spell out as "6 hours" not "6h" — abbreviations don't translate. Also: a 24-hour pill on an app store is unusual; consider "Every 12h" / "Every day" / "Manual only" instead. 3h is very aggressive given GitHub API rate limits. +- **N4.** §3.10 About screen: tagline copy *"Cross-platform app store for GitHub, Codeberg, and Forgejo releases."* is good. Add a localized version with `` placeholders for the three product names so translators don't relocalize "Codeberg". +- **N5.** §3.4 Custom forges row hostname in `Geist Mono labelLarge` — confirm mono font weight reads on the smaller subtitle line below; mono fonts often need +1 weight bump to match proportional siblings. +- **N6.** §3.3 / §4 spec is silent on whether the test request honors per-host PATs (`HostTokenInterceptor`). It should — testing without the token won't catch auth-broken proxies for private GH Enterprise + PAT users. +- **N7.** §6 row: `installer_type_default` → "System installer (Default)" — the parenthetical adds noise. Just "System installer" reads cleaner. The word "Default" was always a non-noun in this UI. +- **N8.** §3.10 "Open source licenses" row labeled as *"placeholder row, hide if not implemented"* — please don't ship a hidden row. Either implement it now (it's a static markdown view; 30 min of work) or omit until done. +- **N9.** §2.2 "icon tile … 40.dp square clipped to `Radii.chip`, background `colorScheme.surfaceContainerHigh`, padded 8.dp, tinted `onSurfaceVariant`" — every row has its own colored tile. The architect listed icons by Material name only; no per-row tile tint. Material 3 settings convention is one tile color (neutral) or one tint per **block** (not per row). Per-row colored tiles add visual noise; the spec already doesn't do this, but the user's brief asked the question — confirm in the spec that tiles are uniformly tinted (not per-icon-color). One line in §2.2. +- **N10.** §4.3 *"Inline helper at the bottom of the form (when expanded): 'Used for fetching repos, downloads, and translations unless you override below.'"* — that's a long sentence under a form. Compress: "Applies to all traffic unless overridden below." +- **N11.** §3.1 *"AMOLED toggle visibility binds to `resolvedDark`, not the raw `isDarkTheme` value"* — call out that this is true for the new `Mode segment` too. The toggle visibility computation depends on the segment selection + system theme. State machine deserves one diagram in spec. +- **N12.** §3.2 the language-tag mono font subtitle (`pl-PL`) is great UX. But for "Follow system" the subtitle should show the *resolved* tag in parens, e.g. "Follow system (en-US)" so the user knows what they're getting. + +--- + +## Things the architect got right + +1. **Splitting "Network" into Connection + Sources.** This is the spec's strongest IA call. The two were always different mental models bolted together. (§1.1 / §1.3) +2. **The hub's dynamic-state subtitle pattern.** Showing "DeepL · auto on" / "Nord · Dark" / "Direct" on the hub row gives users glance-readable state without entering — exactly the right mitigation for the discoverability cost of hub-and-spoke. (§2.2) +3. **Killing the 3× duplicated proxy form.** Whatever wins between Option A / B / C, **anything** is better than three identical 7-field forms. The current code (`feature/tweaks/presentation/components/sections/Network.kt:161-171`) is the worst pattern in the entire feature. The user's complaint here is fully justified and the architect heard it. +4. **The §3.5 Translation provider redesign.** Moving from chips + below-card form to radio rows + revealed credentials card is the right pattern (5 providers, 4 with credentials, 1-of-N selection — that's a radio group, not a chip group). (§3.5) +5. **Section sub-headers in sentence case** (drop `.uppercase()`). Universally correct call; ALLCAPS feels shouty in 2026 UI. (§2.3) +6. **Rejecting `RoundedCornerShape(32.dp)` cards** in favor of `Radii.row` consistency. The current code has three competing shape vocabularies; collapsing to one is overdue. (§5.3) +7. **Empty-state for custom forges with "Add a Forgejo or Gitea host" framing.** Honest copy that names the system. Better than today's generic "Add your own". (§3.4) +8. **§7.4 explicit "things I did not change" list.** Architect's restraint here is good — pinning down what's *not* touched is half of any redesign spec. (Though §7.4's promise to not change `ProxyRepository` schema needs reversal per C2.) + +--- + +## Open questions for product owner + +1. **Telemetry — does it exist today?** (`TelemetryRepository` is injected but there's no UI surface I could find.) If yes, where; if no, do we ship the opt-out in this PR or block on backend? (C1) +2. **Master proxy persistence — is a `ProxyRepository` schema bump acceptable in this phase?** Adds one field per scope (`useMaster: Boolean`) + a fourth master record. (C2) +3. **Search-in-settings + deep links** — pick one, both, or none for v1? (C5) +4. **Library cleanup vs Privacy split** — keep one screen with sub-sections, or split into two hub rows? (M1) +5. **Feedback affordance** — own hub row, topbar overflow, or buried in About? (M7) +6. **Cross-platform-only rows** — hide entirely (architect's pick) or show with "Android only" badge? (M8) +7. **String rename translation policy** — ship in English with `[needs translation]` markers, queue with maintainer translators, or block redesign on 13-locale parity? (M6) +8. **"Direct" rename** — "No proxy" recommended; final call? (C3) +9. **PAC files** — explicit non-goal, or fifth mode pill? (M3) +10. **Restart banner pattern** — adopt persistent banner across all sub-screens for "language pending," "theme migrating," "telemetry consent updated"? (M10) +11. **Hub block grouping** — 4 blocks (architect) or 6–7 tighter blocks (M9)? + +End of review. diff --git a/.design/P12_5_TWEAKS_STRINGS.csv b/.design/P12_5_TWEAKS_STRINGS.csv new file mode 100644 index 000000000..7ebeee0c4 --- /dev/null +++ b/.design/P12_5_TWEAKS_STRINGS.csv @@ -0,0 +1,158 @@ +Resource key,Old English,New English +tweaks_title,Tweaks,Tweaks +section_appearance,Appearance,Appearance +theme_color,Theme color,Palette +theme_light,Light,Light +theme_dark,Dark,Dark +theme_system,System,Follow system +amoled_black_theme,AMOLED black,True black (AMOLED) +amoled_black_description,Use pure black for OLED screens.,Pure-black background — saves power on OLED screens. +system_font,System font,Use system font +system_font_description,Use the system font for the app.,Use your device's default typeface instead of Geist. +scrollbar_option_title,Show scrollbar,Show scrollbar +scrollbar_option_description,Show scrollbar on the right side.,Always show the scrollbar on long pages. +content_width_title,Content width,Content width +content_width_description,Adjust max content width.,How wide content should stretch on big windows. +section_language,Language,Language +language_intro,Choose your app language.,Pick the language used across the app. +language_picker_title,App language,App language +language_picker_description,Restart required after change.,The app restarts when you switch language. +language_follow_system,Follow system,Follow system +language_follow_system_subtitle,(new),Follow system · %1$s +section_network,Network,(retired) +section_connectivity,(new),Connectivity +section_privacy_and_data,(new),Privacy & data +section_app_block,(new),App +section_installs_and_updates,(new),Installs & updates +connection_entry_title,(new),Connection +connection_entry_subtitle_no_proxy,(new),No proxy +connection_entry_subtitle_system,(new),System proxy +connection_entry_subtitle_proxy_with_overrides,(new),%1$s · %2$d override / %1$s · %2$d overrides +sources_entry_title,(new),Sources +proxy_scope_intro,Configure proxies per scope.,(retired) +proxy_scope_discovery_title,Discovery proxy,Search & metadata +proxy_scope_download_title,Download proxy,Downloads +proxy_scope_translation_title,Translation proxy,Translation +proxy_scope_discovery_description,Used for API and search.,"GitHub API, search results, repo details." +proxy_scope_download_description,Used for APK downloads.,APK and asset downloads. +proxy_scope_translation_description,Used for translation providers.,"DeepL, Microsoft, LibreTranslate calls." +proxy_none,None,No proxy +proxy_none_description,No proxy used.,The app connects to the internet without a proxy. +proxy_system,System,System +proxy_system_description,Use system proxy.,Use the proxy configured in your OS. +proxy_http,HTTP,HTTP/HTTPS +proxy_http_caption,(new),Most corporate proxies. +proxy_socks,SOCKS,SOCKS5 +proxy_socks_caption,(new),"Tor, SSH tunnels." +proxy_test,Test,(retired) +proxy_test_main,(new),Test main connection +proxy_test_scope,(new),Test for %1$s +proxy_save,Save,Save +proxy_test_success,Proxy reachable (%d ms),Connected in %1$d ms +proxy_test_main_success,(new),Search ✓ %1$d ms · Downloads ✓ %2$d ms · Translation ✓ %3$d ms +proxy_use_main,(new),Use main +proxy_use_custom,(new),Custom… +proxy_using_main_pill,(new),Using main +proxy_paste_full_url,(new),Paste full URL +proxy_paste_url_placeholder,(new),scheme://user:pass@host:port +proxy_paste_url_error,(new),Couldn't read that URL. +connection_intro_title,(new),How the app reaches the internet +connection_intro_body,(new),Pick a connection mode below. Most people leave this on No proxy. +connection_inline_helper,(new),Applies to all traffic unless overridden below. +connection_overrides_body,(new),Each scope uses the main connection by default. Choose 'Custom' to give a scope its own settings. +mirror_tweaks_entry_label,Mirror,GitHub mirror +custom_forges_entry_label,Custom forges,Custom forges +custom_forges_entry_subtitle,Add your own Forgejo/Gitea hosts.,Add a Forgejo or Gitea host +custom_forges_count (plural),%d hosts,%d host / %d hosts +section_translation,Translation,Translation +translation_intro,Configure translation provider and auto-translate.,Pick the engine the app uses to translate READMEs. +translation_provider_title,Provider,Provider +translation_provider_description,Pick a translation engine.,(retired) +translation_auto_title,Auto-translate,Translate READMEs automatically +translation_auto_subtitle,Translate READMEs when opening repos.,"When opening a repo, translate the README into your target language." +section_installation,INSTALLATION,Install method +install_method_android_only_badge,(new),Android only +install_method_desktop_empty_title,(new),Install method is Android-only +install_method_desktop_empty_body,(new),Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager. +installer_type_default,Default,System installer +installer_type_default_description,Uses Android's default installer.,Asks each time. Works on every device. +installer_type_shizuku,Shizuku,Shizuku +installer_type_shizuku_description,Silent install via Shizuku.,Silent install. Needs Shizuku app running. +installer_type_dhizuku,Dhizuku,Dhizuku +installer_type_dhizuku_description,Silent install via Dhizuku.,Silent install. No root needed. +installer_type_root,Root,Root +installer_type_root_description,Silent install with root.,Silent install via root. Power-user only. +installer_attribution_title,Installer attribution,Pretend the installer is… +installer_attribution_description,Some apps reject silent installs unless installer claims to be Google Play.,Some apps reject silent installs unless the installer claims to be Google Play. This setting controls what we claim. +section_updates,UPDATES,Update behavior +auto_update_title,Auto-update,Install updates in the background +auto_update_description,Apply downloaded updates automatically.,Apply downloaded updates without notifying you each time. +update_check_enabled_title,Background update check,Check automatically +update_check_enabled_description,Periodically check for new releases.,Look for new releases on a schedule. +update_check_interval_title,Check interval,Check every +update_check_interval_description,How often to check for updates.,(retired) +update_interval_6h,(new),Every 6 hours +update_interval_12h,(new),Every 12 hours +update_interval_24h,(new),Daily +update_interval_manual,(new),Manual only +update_desktop_check_on_launch_title,(new),Check on launch +update_desktop_check_on_launch_subtitle,(new),Check for updates each time the app starts. +include_pre_releases_title,Include pre-releases,Include pre-releases +include_pre_releases_description,Treat alpha/beta as available updates.,Show alpha and beta tags as available updates. +skipped_updates_entry_title,Skipped updates,Skipped updates +skipped_updates_count (plural),(new),%d app / %d apps +skipped_updates_empty_subtitle,(new),Nothing skipped +hidden_repositories_title,Hidden repositories,Hidden repositories +hidden_repositories_count (plural),(new),%d repo / %d repos +hidden_repositories_empty_subtitle,(new),No hidden repos +storage,Storage,Storage +downloaded_packages,Downloaded packages,Downloaded APKs +downloaded_packages_description,Installer files kept for resumed updates.,We keep installers around so updates resume fast. +current_size,Current size:,Using: +section_privacy_screen_title,(new),Privacy +privacy_entry_subtitle,(new),Compact dynamic state — see §3.9 +privacy_usage_data_subheader,(new),Usage data +privacy_telemetry_title,(new),Share anonymous usage data +privacy_telemetry_subtitle,(new),Help us understand which features get used. +privacy_telemetry_collect_expand,(new),What we collect +privacy_telemetry_collect_body,(new),App version. OS and platform. Feature usage counts. No repo names. No tokens. No identifiers. +privacy_telemetry_on_snackbar,(new),Sharing usage data starting next launch. +privacy_telemetry_off_snackbar,(new),Usage data sharing stopped. Existing data is dropped. +auto_detect_clipboard_links,Auto-detect clipboard links,Detect repo links in clipboard +auto_detect_clipboard_description,Detect copied repo URLs and offer to open them.,"When you copy a github.com or codeberg.org link, we'll prompt to open it." +privacy_history_subheader,(new),Browsing history +hide_seen_title,Hide seen,Hide repos I've already viewed +hide_seen_description,Skip already-viewed repos in feeds.,Skip seen repos in feeds and search. +clear_seen_history,Clear seen history,Clear viewed history +clear_seen_history_description,Reset the seen-repo list.,Forget which repos you've already opened. +clear_seen_history_confirm,(new),Clear all viewed history? This won't unstar or unfavorite anything. +host_tokens_count (plural),(new),%d token / %d tokens +host_tokens_empty_subtitle,(new),No tokens yet +section_about,About,App info +app_info_tagline,(new),"Cross-platform app store for GitHub, Codeberg, and Forgejo releases." +app_info_action_whats_new,(new),What's new +app_info_action_whats_new_subtitle,(new),Past release notes. +app_info_action_licenses,(new),Open source licenses +app_info_action_licenses_subtitle,(new),Libraries used in the app. +app_info_action_privacy_policy,(new),Privacy policy +app_info_action_privacy_policy_subtitle,(new),View on github-store.org. +app_info_action_source_code,(new),Source code on GitHub +app_info_action_source_code_subtitle,(new),View this app's source. +app_info_version_copied,(new),Version copied. +version,Version,Version +feedback_send,Send feedback,Send feedback +feedback_hub_subtitle,(new),We read every report. +tweaks_search_placeholder,(new),Search settings +tweaks_search_empty,(new),No settings match '%1$s'. +restart_banner_body,(new),Some changes need a restart to apply. +restart_banner_reasons_prefix,(new),Affected: %1$s +restart_banner_reason_language,(new),language +restart_banner_reason_theme,(new),theme +restart_banner_reason_telemetry,(new),usage data +restart_banner_restart_now,(new),Restart now +restart_banner_later,(new),Later +menubar_help_menu,(new),Help +menubar_help_about,(new),About GitHub Store +menubar_help_feedback,(new),Send feedback… +menubar_help_licenses,(new),Open source licenses +menubar_help_privacy,(new),Privacy policy From 54429a2eb96e541504eb14f35e54b7278b06d73f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 18:49:42 +0500 Subject: [PATCH 073/172] feat(core): persist restart-required reasons --- .../data/repository/TweaksRepositoryImpl.kt | 25 +++++++++++++++++++ .../core/domain/model/RestartReason.kt | 7 ++++++ .../domain/repository/TweaksRepository.kt | 7 ++++++ 3 files changed, 39 insertions(+) create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index 2e3a15adf..67d418322 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -28,6 +28,7 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerType +import zed.rainxch.core.domain.model.RestartReason import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.domain.model.TranslationProvider import zed.rainxch.core.domain.repository.TweaksRepository @@ -464,6 +465,29 @@ class TweaksRepositoryImpl( } } + override fun getNeedsRestartReasons(): Flow> = + gatedGetFlow>(K_RESTART_REASONS, emptyList()).map { stored -> + stored.mapNotNull { name -> + runCatching { RestartReason.valueOf(name) }.getOrNull() + }.toSet() + } + + override suspend fun addRestartReason(reason: RestartReason) { + migrationDeferred.await() + rmwLock.withLock { + val current = ksafe.safeGet>(K_RESTART_REASONS, emptyList()).toSet() + if (reason.name in current) return@withLock + ksafe.safePut(K_RESTART_REASONS, (current + reason.name).toList()) + } + } + + override suspend fun clearRestartReasons() { + migrationDeferred.await() + rmwLock.withLock { + ksafe.safePut(K_RESTART_REASONS, emptyList()) + } + } + companion object { private const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L private const val MIGRATION_MARKER = "__migrated_from_datastore_v1__" @@ -512,5 +536,6 @@ class TweaksRepositoryImpl( private const val K_FAVOURITES_SORT_RULE = "favourites_sort_rule" private const val K_CONTENT_WIDTH = "content_width" private const val K_CUSTOM_FORGE_HOSTS = "custom_forge_hosts" + private const val K_RESTART_REASONS = "needs_restart_reasons" } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt new file mode 100644 index 000000000..61d1ceef5 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt @@ -0,0 +1,7 @@ +package zed.rainxch.core.domain.model + +enum class RestartReason { + LANGUAGE, + THEME_MIGRATION, + TELEMETRY_TOGGLE, +} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 7b8db7574..d984b24ae 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -7,6 +7,7 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerType +import zed.rainxch.core.domain.model.RestartReason import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.domain.model.TranslationProvider @@ -192,4 +193,10 @@ interface TweaksRepository { suspend fun addCustomForgeHost(host: String) suspend fun removeCustomForgeHost(host: String) + + fun getNeedsRestartReasons(): Flow> + + suspend fun addRestartReason(reason: RestartReason) + + suspend fun clearRestartReasons() } From 493fe7cc66b900d4ca97870dccb459fa6891be5b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 18:49:46 +0500 Subject: [PATCH 074/172] feat(core): proxy master config and per-scope useMaster --- .../data/repository/ProxyRepositoryImpl.kt | 70 +++++++++++++++++++ .../core/domain/repository/ProxyRepository.kt | 8 +++ 2 files changed, 78 insertions(+) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt index 83343e419..6a20eda94 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt @@ -70,6 +70,21 @@ class ProxyRepositoryImpl( ) } + private object MasterKeys { + const val TYPE = "master_proxy_type" + const val HOST = "master_proxy_host" + const val PORT = "master_proxy_port" + const val USERNAME = "master_proxy_username" + const val PASSWORD = "master_proxy_password" + } + + private fun useMasterKeyFor(scope: ProxyScope): String = + when (scope) { + ProxyScope.DISCOVERY -> "discovery_proxy_use_master" + ProxyScope.DOWNLOAD -> "download_proxy_use_master" + ProxyScope.TRANSLATION -> "translation_proxy_use_master" + } + override fun getProxyConfig(scope: ProxyScope): Flow = flow { migrationDeferred.await() val keys = keysFor(scope) @@ -155,6 +170,61 @@ class ProxyRepositoryImpl( if (value != null) ksafe.safePut(key, value) else ksafe.safeDelete(key) } + override fun getMasterProxyConfig(): Flow = flow { + migrationDeferred.await() + emitAll( + combine( + ksafe.safeGetFlow(MasterKeys.TYPE, null), + ksafe.safeGetFlow(MasterKeys.HOST, null), + ksafe.safeGetFlow(MasterKeys.PORT, null), + ksafe.safeGetFlow(MasterKeys.USERNAME, null), + ksafe.safeGetFlow(MasterKeys.PASSWORD, null), + ) { type, host, port, user, pass -> + if (type == null) null else parseConfig(type, host, port, user, pass) + }, + ) + } + + override suspend fun setMasterProxyConfig(config: ProxyConfig) { + migrationDeferred.await() + when (config) { + is ProxyConfig.None -> { + ksafe.safePut(MasterKeys.TYPE, "none") + ksafe.safeDelete(MasterKeys.HOST); ksafe.safeDelete(MasterKeys.PORT) + ksafe.safeDelete(MasterKeys.USERNAME); ksafe.safeDelete(MasterKeys.PASSWORD) + } + is ProxyConfig.System -> { + ksafe.safePut(MasterKeys.TYPE, "system") + ksafe.safeDelete(MasterKeys.HOST); ksafe.safeDelete(MasterKeys.PORT) + ksafe.safeDelete(MasterKeys.USERNAME); ksafe.safeDelete(MasterKeys.PASSWORD) + } + is ProxyConfig.Http -> { + ksafe.safePut(MasterKeys.TYPE, "http") + ksafe.safePut(MasterKeys.HOST, config.host) + ksafe.safePut(MasterKeys.PORT, config.port) + writeOrClear(MasterKeys.USERNAME, config.username) + writeOrClear(MasterKeys.PASSWORD, config.password) + } + is ProxyConfig.Socks -> { + ksafe.safePut(MasterKeys.TYPE, "socks") + ksafe.safePut(MasterKeys.HOST, config.host) + ksafe.safePut(MasterKeys.PORT, config.port) + writeOrClear(MasterKeys.USERNAME, config.username) + writeOrClear(MasterKeys.PASSWORD, config.password) + } + } + } + + override fun getUseMaster(scope: ProxyScope): Flow = flow { + migrationDeferred.await() + emitAll(ksafe.safeGetFlow(useMasterKeyFor(scope), false)) + } + + override suspend fun setUseMaster(scope: ProxyScope, useMaster: Boolean) { + migrationDeferred.await() + ksafe.safePut(useMasterKeyFor(scope), useMaster) + } + private suspend fun migrateIfNeeded() { if (migrated) return migrationLock.withLock { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt index a1c6aa0d9..f37e6c440 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt @@ -11,4 +11,12 @@ interface ProxyRepository { scope: ProxyScope, config: ProxyConfig, ) + + fun getMasterProxyConfig(): Flow + + suspend fun setMasterProxyConfig(config: ProxyConfig) + + fun getUseMaster(scope: ProxyScope): Flow + + suspend fun setUseMaster(scope: ProxyScope, useMaster: Boolean) } From 47b64122400a1f006af2df27ccf5608550db8bbc Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 18:49:47 +0500 Subject: [PATCH 075/172] feat(nav): add Tweaks sub-screen routes --- .../app/navigation/GithubStoreGraph.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index 7015c30c7..6751602a8 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -35,6 +35,39 @@ sealed interface GithubStoreGraph { @Serializable data object TweaksScreen : GithubStoreGraph + @Serializable + data object TweaksAppearanceScreen : GithubStoreGraph + + @Serializable + data object TweaksLanguageScreen : GithubStoreGraph + + @Serializable + data object TweaksConnectionScreen : GithubStoreGraph + + @Serializable + data object TweaksSourcesScreen : GithubStoreGraph + + @Serializable + data object TweaksTranslationScreen : GithubStoreGraph + + @Serializable + data object TweaksInstallScreen : GithubStoreGraph + + @Serializable + data object TweaksUpdatesScreen : GithubStoreGraph + + @Serializable + data object TweaksStorageScreen : GithubStoreGraph + + @Serializable + data object TweaksPrivacyScreen : GithubStoreGraph + + @Serializable + data object TweaksAppInfoScreen : GithubStoreGraph + + @Serializable + data object LicensesScreen : GithubStoreGraph + @Serializable data object FavouritesScreen : GithubStoreGraph From b21e8b1111b64e87007c0abb8339268518ebbd5a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 18:49:51 +0500 Subject: [PATCH 076/172] feat(tweaks): hub-and-spoke shell with search and restart banner --- .../app/navigation/AppNavigation.kt | 129 ++++- .../composeResources/values/strings.xml | 36 ++ .../tweaks/presentation/TweaksAction.kt | 3 + .../rainxch/tweaks/presentation/TweaksRoot.kt | 516 ++++++++++++------ .../tweaks/presentation/TweaksState.kt | 7 + .../tweaks/presentation/TweaksViewModel.kt | 25 + .../presentation/components/RestartBanner.kt | 109 ++++ .../presentation/components/TweaksEntryRow.kt | 142 +++++ .../components/TweaksSearchField.kt | 65 +++ .../components/TweaksStubScreen.kt | 96 ++++ 10 files changed, 944 insertions(+), 184 deletions(-) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSearchField.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksStubScreen.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index c8814d63c..093cd947e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -55,6 +55,16 @@ import zed.rainxch.githubstore.app.whatsnew.WhatsNewViewModel import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.adaptive_pick_repo_subtitle import zed.rainxch.githubstore.core.presentation.res.adaptive_pick_repo_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_app_info +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_appearance +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_connection +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_install_method +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_language +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_privacy +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_sources +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_storage +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_translation +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_updates import zed.rainxch.home.presentation.HomeRoot import zed.rainxch.profile.presentation.ProfileRoot import zed.rainxch.recentlyviewed.presentation.RecentlyViewedRoot @@ -63,6 +73,7 @@ import zed.rainxch.search.presentation.mappers.toSearchPlatformUi import zed.rainxch.search.presentation.model.SearchPlatformUi import zed.rainxch.starred.presentation.StarredReposRoot import zed.rainxch.tweaks.presentation.TweaksRoot +import zed.rainxch.tweaks.presentation.components.TweaksStubScreen import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.hosttokens.HostTokensRoot import zed.rainxch.tweaks.presentation.mirror.MirrorPickerRoot @@ -644,18 +655,49 @@ fun AppNavigation( composable { TweaksRoot( - onNavigateToMirrorPicker = { - navController.navigate(GithubStoreGraph.MirrorPickerScreen) { + onNavigateBack = { navController.popBackStack() }, + onNavigateToAppearance = { + navController.navigate(GithubStoreGraph.TweaksAppearanceScreen) { + launchSingleTop = true + } + }, + onNavigateToLanguage = { + navController.navigate(GithubStoreGraph.TweaksLanguageScreen) { + launchSingleTop = true + } + }, + onNavigateToConnection = { + navController.navigate(GithubStoreGraph.TweaksConnectionScreen) { + launchSingleTop = true + } + }, + onNavigateToSources = { + navController.navigate(GithubStoreGraph.TweaksSourcesScreen) { + launchSingleTop = true + } + }, + onNavigateToTranslation = { + navController.navigate(GithubStoreGraph.TweaksTranslationScreen) { + launchSingleTop = true + } + }, + onNavigateToInstallMethod = { + navController.navigate(GithubStoreGraph.TweaksInstallScreen) { launchSingleTop = true } }, - onNavigateToSkippedUpdates = { - navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { + onNavigateToUpdates = { + navController.navigate(GithubStoreGraph.TweaksUpdatesScreen) { launchSingleTop = true } }, - onNavigateToHiddenRepositories = { - navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + onNavigateToStorage = { + navController.navigate(GithubStoreGraph.TweaksStorageScreen) { + launchSingleTop = true + } + }, + onNavigateToPrivacy = { + navController.navigate(GithubStoreGraph.TweaksPrivacyScreen) { launchSingleTop = true } }, @@ -664,6 +706,81 @@ fun AppNavigation( launchSingleTop = true } }, + onNavigateToAppInfo = { + navController.navigate(GithubStoreGraph.TweaksAppInfoScreen) { + launchSingleTop = true + } + }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_appearance), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_language), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_connection), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_sources), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_translation), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_install_method), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_updates), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_storage), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_privacy), + onNavigateBack = { navController.popBackStack() }, + ) + } + + composable { + TweaksStubScreen( + title = stringResource(Res.string.tweaks_entry_app_info), + onNavigateBack = { navController.popBackStack() }, ) } diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index cfc9eba7e..7c4d54c24 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1195,6 +1195,42 @@ Remove filter App settings, theme, network, translation, advanced options. + + Connectivity + Privacy & data + App + Installs & updates + Look & feel + Appearance + Language + Connection + Sources + Translation + Install method + Update behavior + Storage + Privacy + Access tokens + App info + Send feedback + Tap to manage + Search settings + No settings match \'%1$s\'. + We read every report. + Some changes need a restart to apply. + Affected: %1$s + language + theme + usage data + Restart now + Later + Android only + Install method is Android-only + Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager. + Coming in a follow-up + This screen is part of the in-progress redesign. The settings still work — they\'re just being moved here from the old layout. + Back + APK Inspect Inspect APK diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index f9c351aba..bb6c42c04 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -209,4 +209,7 @@ sealed interface TweaksAction { data class OnCustomForgeDraftChanged(val draft: String) : TweaksAction data object OnAddCustomForge : TweaksAction data class OnRemoveCustomForge(val host: String) : TweaksAction + + data object OnRestartNowClick : TweaksAction + data object OnRestartLaterClick : TweaksAction } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 3bf928265..781313afb 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -1,13 +1,33 @@ package zed.rainxch.tweaks.presentation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.GTranslate +import androidx.compose.material.icons.outlined.Hub +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.InstallMobile +import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material.icons.outlined.PrivacyTip +import androidx.compose.material.icons.outlined.SearchOff +import androidx.compose.material.icons.outlined.Translate +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material.icons.outlined.VpnKey +import androidx.compose.material.icons.outlined.Wifi import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -18,10 +38,16 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner @@ -35,24 +61,33 @@ import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.tweaks.presentation.components.ClearDownloadsDialog -import zed.rainxch.tweaks.presentation.components.sections.about -import zed.rainxch.tweaks.presentation.components.sections.othersSection -import zed.rainxch.tweaks.presentation.components.sections.settings +import zed.rainxch.tweaks.presentation.components.RestartBanner +import zed.rainxch.tweaks.presentation.components.SectionHeader +import zed.rainxch.tweaks.presentation.components.TweaksEntryRow +import zed.rainxch.tweaks.presentation.components.TweaksSearchField import zed.rainxch.tweaks.presentation.feedback.components.FeedbackBottomSheet import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel @Composable fun TweaksRoot( - onNavigateToMirrorPicker: () -> Unit, - onNavigateToSkippedUpdates: () -> Unit, - onNavigateToHiddenRepositories: () -> Unit, - onNavigateToHostTokens: () -> Unit = {}, + onNavigateBack: () -> Unit, + onNavigateToAppearance: () -> Unit, + onNavigateToLanguage: () -> Unit, + onNavigateToConnection: () -> Unit, + onNavigateToSources: () -> Unit, + onNavigateToTranslation: () -> Unit, + onNavigateToInstallMethod: () -> Unit, + onNavigateToUpdates: () -> Unit, + onNavigateToStorage: () -> Unit, + onNavigateToPrivacy: () -> Unit, + onNavigateToHostTokens: () -> Unit, + onNavigateToAppInfo: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() val snackbarState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + var feedbackSheetOpen by rememberSaveable { mutableStateOf(false) } val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { @@ -60,7 +95,6 @@ fun TweaksRoot( androidx.lifecycle.LifecycleEventObserver { _, event -> if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) { viewModel.onAction(TweaksAction.OnRefreshCacheSize) - viewModel.onAction(TweaksAction.OnReevaluateBatteryOptimizationCard) } } @@ -72,145 +106,52 @@ fun TweaksRoot( ObserveAsEvents(viewModel.events) { event -> when (event) { - TweaksEvent.OnProxySaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.proxy_saved)) - } - } - - is TweaksEvent.OnProxySaveError -> { - coroutineScope.launch { - snackbarState.showSnackbar(event.message) - } - } - - is TweaksEvent.OnProxyTestSuccess -> { - coroutineScope.launch { - snackbarState.showSnackbar( - getString(Res.string.proxy_test_success, event.latencyMs), - ) - } - } - - is TweaksEvent.OnProxyTestError -> { - coroutineScope.launch { - snackbarState.showSnackbar(event.message) - } - } - - TweaksEvent.OnCacheCleared -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.downloads_cleared)) - } - } - - is TweaksEvent.OnCacheClearError -> { - coroutineScope.launch { - snackbarState.showSnackbar(event.message) - } - } - - TweaksEvent.OnSeenHistoryCleared -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.seen_history_cleared)) - } - } - - TweaksEvent.OnTranslationProviderSaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.translation_provider_saved)) - } - } - - TweaksEvent.OnYoudaoCredentialsSaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.translation_youdao_saved)) - } - } - - TweaksEvent.OnLibreTranslateCredentialsSaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.translation_libre_saved)) - } - } - - TweaksEvent.OnDeeplCredentialsSaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.translation_deepl_saved)) - } - } - - TweaksEvent.OnMicrosoftTranslatorCredentialsSaved -> { - coroutineScope.launch { - snackbarState.showSnackbar(getString(Res.string.translation_microsoft_saved)) - } - } - TweaksEvent.OnAppLanguageChangeRequiresRestart -> { coroutineScope.launch { - val result = - snackbarState.showSnackbar( - message = getString(Res.string.language_restart_required), - actionLabel = getString(Res.string.language_restart_action), - withDismissAction = true, - ) + val result = snackbarState.showSnackbar( + message = getString(Res.string.language_restart_required), + actionLabel = getString(Res.string.language_restart_action), + withDismissAction = true, + ) if (result == SnackbarResult.ActionPerformed) { restartAppAfterLanguageChange() } } } + else -> Unit } } - TweaksScreen( + TweaksHubScreen( state = state, - onAction = { action -> - when (action) { - TweaksAction.OnMirrorPickerClick -> onNavigateToMirrorPicker() - TweaksAction.OnSkippedUpdatesClick -> onNavigateToSkippedUpdates() - - TweaksAction.OnHiddenRepositoriesClick -> onNavigateToHiddenRepositories() - else -> viewModel.onAction(action) - } - }, - snackbarState = snackbarState, + onNavigateBack = onNavigateBack, + onNavigateToAppearance = onNavigateToAppearance, + onNavigateToLanguage = onNavigateToLanguage, + onNavigateToConnection = onNavigateToConnection, + onNavigateToSources = onNavigateToSources, + onNavigateToTranslation = onNavigateToTranslation, + onNavigateToInstallMethod = onNavigateToInstallMethod, + onNavigateToUpdates = onNavigateToUpdates, + onNavigateToStorage = onNavigateToStorage, + onNavigateToPrivacy = onNavigateToPrivacy, onNavigateToHostTokens = onNavigateToHostTokens, + onNavigateToAppInfo = onNavigateToAppInfo, + onSendFeedbackClick = { feedbackSheetOpen = true }, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + snackbarState = snackbarState, ) - if (state.isClearDownloadsDialogVisible) { - ClearDownloadsDialog( - cacheSize = state.cacheSize, - onDismissRequest = { - viewModel.onAction(TweaksAction.OnClearDownloadsDismiss) - }, - onConfirm = { - viewModel.onAction(TweaksAction.OnClearDownloadsConfirm) - }, - ) - } - - if (state.showCustomForgesDialog) { - zed.rainxch.tweaks.presentation.components.CustomForgesDialog( - state = state, - onAction = { viewModel.onAction(it) }, - ) - } - - if (state.isFeedbackSheetVisible) { + if (feedbackSheetOpen) { FeedbackBottomSheet( - onDismiss = { - viewModel.onAction(TweaksAction.OnFeedbackDismiss) - }, + onDismiss = { feedbackSheetOpen = false }, onSent = { channel -> - viewModel.onAction(TweaksAction.OnFeedbackDismiss) + feedbackSheetOpen = false coroutineScope.launch { - val msg = - when (channel) { - FeedbackChannel.EMAIL -> - getString(Res.string.feedback_send_success_email) - FeedbackChannel.GITHUB -> - getString(Res.string.feedback_send_success_github) - } + val msg = when (channel) { + FeedbackChannel.EMAIL -> getString(Res.string.feedback_send_success_email) + FeedbackChannel.GITHUB -> getString(Res.string.feedback_send_success_github) + } snackbarState.showSnackbar(msg) } }, @@ -225,15 +166,160 @@ fun TweaksRoot( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +private data class TweaksHubEntry( + val title: String, + val subtitle: String, + val icon: ImageVector, + val onClick: () -> Unit, +) + +private data class TweaksHubBlock( + val title: String, + val entries: List, +) + +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun TweaksScreen( +fun TweaksHubScreen( state: TweaksState, - onAction: (TweaksAction) -> Unit, + onNavigateBack: () -> Unit, + onNavigateToAppearance: () -> Unit, + onNavigateToLanguage: () -> Unit, + onNavigateToConnection: () -> Unit, + onNavigateToSources: () -> Unit, + onNavigateToTranslation: () -> Unit, + onNavigateToInstallMethod: () -> Unit, + onNavigateToUpdates: () -> Unit, + onNavigateToStorage: () -> Unit, + onNavigateToPrivacy: () -> Unit, + onNavigateToHostTokens: () -> Unit, + onNavigateToAppInfo: () -> Unit, + onSendFeedbackClick: () -> Unit, + onRestartNow: () -> Unit, + onRestartLater: () -> Unit, snackbarState: SnackbarHostState, - onNavigateToHostTokens: () -> Unit = {}, ) { val bottomNavHeight = LocalBottomNavigationHeight.current + var query by rememberSaveable { mutableStateOf("") } + + val tapToManage = stringResource(Res.string.tweaks_entry_subtitle_tap) + + val blocks = listOf( + TweaksHubBlock( + title = stringResource(Res.string.section_look_and_feel), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_appearance), + subtitle = tapToManage, + icon = Icons.Outlined.Palette, + onClick = onNavigateToAppearance, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_language), + subtitle = tapToManage, + icon = Icons.Outlined.Translate, + onClick = onNavigateToLanguage, + ), + ), + ), + TweaksHubBlock( + title = stringResource(Res.string.section_connectivity), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_connection), + subtitle = tapToManage, + icon = Icons.Outlined.Wifi, + onClick = onNavigateToConnection, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_sources), + subtitle = tapToManage, + icon = Icons.Outlined.Hub, + onClick = onNavigateToSources, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_translation), + subtitle = tapToManage, + icon = Icons.Outlined.GTranslate, + onClick = onNavigateToTranslation, + ), + ), + ), + TweaksHubBlock( + title = stringResource(Res.string.section_installs_and_updates), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_install_method), + subtitle = tapToManage, + icon = Icons.Outlined.InstallMobile, + onClick = onNavigateToInstallMethod, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_updates), + subtitle = tapToManage, + icon = Icons.Outlined.Update, + onClick = onNavigateToUpdates, + ), + ), + ), + TweaksHubBlock( + title = stringResource(Res.string.section_privacy_and_data), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_storage), + subtitle = state.cacheSize.ifBlank { tapToManage }, + icon = Icons.Outlined.Inventory2, + onClick = onNavigateToStorage, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_privacy), + subtitle = tapToManage, + icon = Icons.Outlined.PrivacyTip, + onClick = onNavigateToPrivacy, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_access_tokens), + subtitle = tapToManage, + icon = Icons.Outlined.VpnKey, + onClick = onNavigateToHostTokens, + ), + ), + ), + TweaksHubBlock( + title = stringResource(Res.string.section_app_block), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_app_info), + subtitle = state.versionName.ifBlank { "—" }, + icon = Icons.Outlined.Info, + onClick = onNavigateToAppInfo, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_feedback), + subtitle = stringResource(Res.string.feedback_hub_subtitle), + icon = Icons.Outlined.Feedback, + onClick = onSendFeedbackClick, + ), + ), + ), + ) + + val filteredBlocks = remember(query, blocks) { + if (query.isBlank()) { + blocks + } else { + val q = query.trim() + blocks.map { block -> + block.copy( + entries = block.entries.filter { entry -> + entry.title.contains(q, ignoreCase = true) || + entry.subtitle.contains(q, ignoreCase = true) + }, + ) + }.filter { it.entries.isNotEmpty() } + } + } + Scaffold( snackbarHost = { SnackbarHost( @@ -242,78 +328,152 @@ fun TweaksScreen( ) }, topBar = { - TopAppBar() + TopAppBar( + title = { + Text( + text = stringResource(Res.string.tweaks_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back_cd), + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> val listState = rememberLazyListState() LazyColumn( state = listState, - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(16.dp) - .arrowKeyScroll(listState, autoFocus = true), + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp) + .arrowKeyScroll(listState, autoFocus = true), ) { - settings( - state = state, - onAction = onAction, - ) + if (state.restartBannerVisible) { + item(key = "restart_banner") { + Spacer(Modifier.height(8.dp)) + RestartBanner( + reasons = state.needsRestartReasons, + onRestartNow = onRestartNow, + onLater = onRestartLater, + ) + } + } - item { - Spacer(Modifier.height(16.dp)) - zed.rainxch.tweaks.presentation.hosttokens.HostTokensEntryCard( - onClick = onNavigateToHostTokens, + item(key = "search_field") { + Spacer(Modifier.height(12.dp)) + TweaksSearchField( + query = query, + onQueryChange = { query = it }, + onClear = { query = "" }, ) Spacer(Modifier.height(16.dp)) } - othersSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(32.dp)) + if (filteredBlocks.isEmpty()) { + item(key = "search_empty") { + EmptySearchResult(query = query) + Spacer(Modifier.height(32.dp)) + } + } else if (query.isBlank()) { + blocks.forEachIndexed { idx, block -> + item(key = "block_header_${block.title}") { + if (idx > 0) Spacer(Modifier.height(24.dp)) + SectionHeader(text = block.title) + Spacer(Modifier.height(8.dp)) + } + block.entries.forEach { entry -> + item(key = "entry_${block.title}_${entry.title}") { + TweaksEntryRow( + title = entry.title, + subtitle = entry.subtitle, + icon = entry.icon, + onClick = entry.onClick, + ) + Spacer(Modifier.height(8.dp)) + } + } + } + } else { + filteredBlocks.flatMap { it.entries }.forEach { entry -> + item(key = "search_entry_${entry.title}") { + TweaksEntryRow( + title = entry.title, + subtitle = entry.subtitle, + icon = entry.icon, + onClick = entry.onClick, + ) + Spacer(Modifier.height(8.dp)) + } + } } - about( - versionName = state.versionName, - onAction = onAction, - ) - - item { + item(key = "bottom_spacer") { Spacer(Modifier.height(bottomNavHeight + 32.dp)) } } } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun TopAppBar() { - TopAppBar( - title = { +private fun EmptySearchResult(query: String) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Outlined.SearchOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) Text( - text = stringResource(Res.string.tweaks_title), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold, - fontSize = androidx.compose.ui.unit.TextUnit(22f, androidx.compose.ui.unit.TextUnitType.Sp), - ), - color = MaterialTheme.colorScheme.onBackground, + text = stringResource(Res.string.tweaks_search_empty, query), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) - }, - ) + } + } } @Preview @Composable private fun Preview() { GithubStoreTheme { - TweaksScreen( + TweaksHubScreen( state = TweaksState(), - onAction = {}, + onNavigateBack = {}, + onNavigateToAppearance = {}, + onNavigateToLanguage = {}, + onNavigateToConnection = {}, + onNavigateToSources = {}, + onNavigateToTranslation = {}, + onNavigateToInstallMethod = {}, + onNavigateToUpdates = {}, + onNavigateToStorage = {}, + onNavigateToPrivacy = {}, + onNavigateToHostTokens = {}, + onNavigateToAppInfo = {}, + onSendFeedbackClick = {}, + onRestartNow = {}, + onRestartLater = {}, snackbarState = SnackbarHostState(), ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 7e08fedf3..038210a97 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -7,6 +7,7 @@ import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerAttribution import zed.rainxch.core.domain.model.InstallerType import zed.rainxch.core.domain.model.ProxyScope +import zed.rainxch.core.domain.model.RestartReason import zed.rainxch.core.domain.model.RootAvailability import zed.rainxch.core.domain.model.ShizukuAvailability import zed.rainxch.core.domain.model.TranslationProvider @@ -63,8 +64,14 @@ data class TweaksState( val showCustomForgesDialog: Boolean = false, val customForgeDraft: String = "", val customForgeError: String? = null, + val needsRestartReasons: Set = emptySet(), + val restartBannerSessionDismissed: Boolean = false, ) { + val restartBannerVisible: Boolean + get() = needsRestartReasons.isNotEmpty() && !restartBannerSessionDismissed + + val displayedTranslationProvider: TranslationProvider get() = draftTranslationProvider ?: translationProvider diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 38ae17b37..fe1ed9230 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -105,6 +105,7 @@ class TweaksViewModel( observeDhizukuStatus() observeRootStatus() observeInstallerAttribution() + observeNeedsRestartReasons() hasLoadedInitialData = true } @@ -1067,6 +1068,11 @@ class TweaksViewModel( if (action.tag == _state.value.selectedAppLanguage) return viewModelScope.launch { tweaksRepository.setAppLanguage(action.tag) + runCatching { + tweaksRepository.addRestartReason( + zed.rainxch.core.domain.model.RestartReason.LANGUAGE, + ) + } if (getPlatform() != Platform.ANDROID) { _events.send(TweaksEvent.OnAppLanguageChangeRequiresRestart) } @@ -1170,6 +1176,25 @@ class TweaksViewModel( } } } + + TweaksAction.OnRestartNowClick -> { + viewModelScope.launch { + runCatching { tweaksRepository.clearRestartReasons() } + restartAppAfterLanguageChange() + } + } + + TweaksAction.OnRestartLaterClick -> { + _state.update { it.copy(restartBannerSessionDismissed = true) } + } + } + } + + private fun observeNeedsRestartReasons() { + viewModelScope.launch { + tweaksRepository.getNeedsRestartReasons().collect { reasons -> + _state.update { it.copy(needsRestartReasons = reasons) } + } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt new file mode 100644 index 000000000..5090788fc --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt @@ -0,0 +1,109 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.domain.model.RestartReason +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.restart_banner_body +import zed.rainxch.githubstore.core.presentation.res.restart_banner_later +import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_language +import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_telemetry +import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_theme +import zed.rainxch.githubstore.core.presentation.res.restart_banner_reasons_prefix +import zed.rainxch.githubstore.core.presentation.res.restart_banner_restart_now + +@Composable +fun RestartBanner( + reasons: Set, + onRestartNow: () -> Unit, + onLater: () -> Unit, + modifier: Modifier = Modifier, +) { + if (reasons.isEmpty()) return + + val reasonLabels = reasons.map { reason -> + stringResource( + when (reason) { + RestartReason.LANGUAGE -> Res.string.restart_banner_reason_language + RestartReason.THEME_MIGRATION -> Res.string.restart_banner_reason_theme + RestartReason.TELEMETRY_TOGGLE -> Res.string.restart_banner_reason_telemetry + }, + ) + } + + Surface( + modifier = modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.tertiaryContainer, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + ), + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(Res.string.restart_banner_body), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onTertiaryContainer, + ) + if (reasonLabels.isNotEmpty()) { + Text( + text = stringResource( + Res.string.restart_banner_reasons_prefix, + reasonLabels.joinToString(", "), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiaryContainer, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = onLater) { + Text( + text = stringResource(Res.string.restart_banner_later), + color = MaterialTheme.colorScheme.onTertiaryContainer, + ) + } + Spacer(Modifier.height(0.dp)) + FilledTonalButton( + onClick = onRestartNow, + shape = WonkySquircleShape.CtaPrimary, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text(text = stringResource(Res.string.restart_banner_restart_now)) + } + } + } + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt new file mode 100644 index 000000000..f1e26ce94 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt @@ -0,0 +1,142 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +@Composable +fun TweaksEntryRow( + title: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + badge: (@Composable () -> Unit)? = null, +) { + Surface( + modifier = modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick) + .semantics { + role = Role.Button + contentDescription = buildString { + append(title) + if (!subtitle.isNullOrBlank()) { + append(". ") + append(subtitle) + } + append(". Double-tap to open.") + } + }, + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (!subtitle.isNullOrBlank()) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + if (badge != null) { + badge() + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } +} + +@Composable +fun TweaksEntryBadge(text: String) { + Surface( + shape = Radii.chip, + color = MaterialTheme.colorScheme.tertiaryContainer, + ) { + Text( + text = text, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSearchField.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSearchField.kt new file mode 100644 index 000000000..b6d17e5e4 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSearchField.kt @@ -0,0 +1,65 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_search_placeholder + +@Composable +fun TweaksSearchField( + query: String, + onQueryChange: (String) -> Unit, + onClear: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier.fillMaxWidth(), + placeholder = { + Text( + text = stringResource(Res.string.tweaks_search_placeholder), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = onClear) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + singleLine = true, + shape = WonkySquircleShape.Search, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + focusedBorderColor = MaterialTheme.colorScheme.outline, + unfocusedBorderColor = MaterialTheme.colorScheme.outline, + ), + ) +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksStubScreen.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksStubScreen.kt new file mode 100644 index 000000000..7a46f0708 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksStubScreen.kt @@ -0,0 +1,96 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.Construction +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.back_cd +import zed.rainxch.githubstore.core.presentation.res.tweaks_stub_coming_soon_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_stub_coming_soon_title + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TweaksStubScreen( + title: String, + onNavigateBack: () -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back_cd), + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) + }, + containerColor = MaterialTheme.colorScheme.background, + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 32.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + imageVector = Icons.Outlined.Construction, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(48.dp), + ) + Text( + text = stringResource(Res.string.tweaks_stub_coming_soon_title), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(Res.string.tweaks_stub_coming_soon_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } +} From 06016069500f9e19dd2594cece5207228f94d11c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:08 +0500 Subject: [PATCH 077/172] feat(tweaks): shared sub-screen scaffold with restart banner --- .../components/TweaksSubScreenScaffold.kt | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt new file mode 100644 index 000000000..888b99085 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt @@ -0,0 +1,100 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.domain.model.RestartReason +import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight +import zed.rainxch.core.presentation.utils.arrowKeyScroll +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.back_cd + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TweaksSubScreenScaffold( + title: String, + onNavigateBack: () -> Unit, + snackbarState: SnackbarHostState, + restartReasons: Set, + onRestartNow: () -> Unit, + onRestartLater: () -> Unit, + showRestartBanner: Boolean, + content: LazyListScope.() -> Unit, +) { + val bottomNavHeight = LocalBottomNavigationHeight.current + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back_cd), + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) + }, + snackbarHost = { + SnackbarHost( + hostState = snackbarState, + modifier = Modifier.padding(bottom = bottomNavHeight + 16.dp), + ) + }, + containerColor = MaterialTheme.colorScheme.background, + ) { innerPadding -> + val listState = rememberLazyListState() + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp) + .arrowKeyScroll(listState, autoFocus = true), + contentPadding = PaddingValues(top = 8.dp, bottom = bottomNavHeight + 32.dp), + ) { + if (showRestartBanner && restartReasons.isNotEmpty()) { + item(key = "restart_banner") { + RestartBanner( + reasons = restartReasons, + onRestartNow = onRestartNow, + onLater = onRestartLater, + ) + Spacer(Modifier.height(16.dp)) + } + } + content() + } + } +} From f409e238f7e82f404d98f400db8c5cb0e6b1dcd8 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:09 +0500 Subject: [PATCH 078/172] feat(tweaks): Appearance sub-screen --- .../appearance/TweaksAppearanceRoot.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appearance/TweaksAppearanceRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appearance/TweaksAppearanceRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appearance/TweaksAppearanceRoot.kt new file mode 100644 index 000000000..ce57eb238 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appearance/TweaksAppearanceRoot.kt @@ -0,0 +1,39 @@ +package zed.rainxch.tweaks.presentation.appearance + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_appearance +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold +import zed.rainxch.tweaks.presentation.components.sections.appearanceSection + +@Composable +fun TweaksAppearanceRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_appearance), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + appearanceSection( + state = state, + onAction = { viewModel.onAction(it) }, + ) + } +} From ce440015d4e2fc706311f8d9c86f19fea81c54b1 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:10 +0500 Subject: [PATCH 079/172] feat(tweaks): Language sub-screen with searchable list --- .../language/TweaksLanguageRoot.kt | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt new file mode 100644 index 000000000..c8506d742 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt @@ -0,0 +1,214 @@ +package zed.rainxch.tweaks.presentation.language + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.outlined.PhoneAndroid +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.model.AppLanguages +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.language_follow_system +import zed.rainxch.githubstore.core.presentation.res.language_picker_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_language +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSearchField +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +@Composable +fun TweaksLanguageRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + var query by rememberSaveable { mutableStateOf("") } + + val languages = AppLanguages.ALL + val filtered by remember(query) { + derivedStateOf { + if (query.isBlank()) languages + else languages.filter { + it.displayName.contains(query, ignoreCase = true) || + it.tag.contains(query, ignoreCase = true) + } + } + } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_language), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "language_intro") { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(Res.string.language_picker_title), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "The app restarts when you switch language.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.height(12.dp)) + } + + item(key = "language_search") { + TweaksSearchField( + query = query, + onQueryChange = { query = it }, + onClear = { query = "" }, + ) + Spacer(Modifier.height(12.dp)) + } + + if (query.isBlank()) { + item(key = "follow_system_row") { + LanguageRow( + title = stringResource(Res.string.language_follow_system), + subtitleTag = null, + leadingIcon = true, + selected = state.selectedAppLanguage == null, + onClick = { viewModel.onAction(TweaksAction.OnAppLanguageSelected(null)) }, + ) + Spacer(Modifier.height(8.dp)) + } + } + + filtered.forEach { language -> + item(key = "lang_${language.tag}") { + LanguageRow( + title = language.displayName, + subtitleTag = language.tag, + leadingIcon = false, + selected = state.selectedAppLanguage == language.tag, + onClick = { + viewModel.onAction(TweaksAction.OnAppLanguageSelected(language.tag)) + }, + ) + Spacer(Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun LanguageRow( + title: String, + subtitleTag: String?, + leadingIcon: Boolean, + selected: Boolean, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (leadingIcon) { + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.PhoneAndroid, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (!subtitleTag.isNullOrBlank()) { + Text( + text = subtitleTag, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (selected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp), + ) + } + } + } +} From 183ca675118975031c14f178443e37bc67519170 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:11 +0500 Subject: [PATCH 080/172] feat(tweaks): Storage sub-screen for downloaded APKs --- .../presentation/storage/TweaksStorageRoot.kt | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt new file mode 100644 index 000000000..3ee05b4b4 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt @@ -0,0 +1,186 @@ +package zed.rainxch.tweaks.presentation.storage + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.downloads_cleared +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_storage +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksEvent +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.ClearDownloadsDialog +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +@Composable +fun TweaksStorageRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + ObserveAsEvents(viewModel.events) { event -> + when (event) { + TweaksEvent.OnCacheCleared -> { + coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.downloads_cleared)) + } + } + is TweaksEvent.OnCacheClearError -> { + coroutineScope.launch { + snackbarState.showSnackbar(event.message) + } + } + else -> Unit + } + } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_storage), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "storage_card") { + DownloadsCard( + cacheSize = state.cacheSize, + onClearClick = { viewModel.onAction(TweaksAction.OnClearCacheClick) }, + ) + } + } + + if (state.isClearDownloadsDialogVisible) { + ClearDownloadsDialog( + cacheSize = state.cacheSize, + onDismissRequest = { viewModel.onAction(TweaksAction.OnClearDownloadsDismiss) }, + onConfirm = { viewModel.onAction(TweaksAction.OnClearDownloadsConfirm) }, + ) + } +} + +@Composable +private fun DownloadsCard( + cacheSize: String, + onClearClick: () -> Unit, +) { + val sizeDisplay = cacheSize.ifBlank { "0 B" } + val isEmpty = sizeDisplay == "0 B" + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Inventory2, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = "Downloaded APKs", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "We keep installers around so updates resume fast.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Using: $sizeDisplay", + style = MaterialTheme.typography.labelMedium.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + FilledTonalButton( + onClick = onClearClick, + enabled = !isEmpty, + shape = WonkySquircleShape.CtaAlt, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Icon( + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(4.dp)) + Text(text = "Clear") + } + } + } +} From 458efa94f543e5975d358422ac32dc565c83e8ae Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:13 +0500 Subject: [PATCH 081/172] feat(tweaks): App info and Licenses sub-screens --- .../presentation/appinfo/TweaksAppInfoRoot.kt | 247 ++++++++++++++++++ .../presentation/licenses/LicensesRoot.kt | 149 +++++++++++ 2 files changed, 396 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt new file mode 100644 index 000000000..c64978da8 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -0,0 +1,247 @@ +package zed.rainxch.tweaks.presentation.appinfo + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.material.icons.outlined.Store +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_app_info +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +private const val PRIVACY_POLICY_URL = "https://github-store.org/privacy" +private const val SOURCE_CODE_URL = "https://github.com/OpenHub-Store/GitHub-Store" + +@Composable +fun TweaksAppInfoRoot( + onNavigateBack: () -> Unit, + onNavigateToWhatsNewHistory: () -> Unit, + onNavigateToLicenses: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val uriHandler = LocalUriHandler.current + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_app_info), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "app_identity") { + AppIdentityCard(versionName = state.versionName) + Spacer(Modifier.height(16.dp)) + } + + item(key = "action_whats_new") { + ActionRow( + icon = Icons.Outlined.NewReleases, + title = "What's new", + subtitle = "Past release notes.", + onClick = onNavigateToWhatsNewHistory, + ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "action_licenses") { + ActionRow( + icon = Icons.Outlined.Code, + title = "Open source licenses", + subtitle = "Libraries used in the app.", + onClick = onNavigateToLicenses, + ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "action_privacy") { + ActionRow( + icon = Icons.Outlined.Description, + title = "Privacy policy", + subtitle = "View on github-store.org.", + onClick = { + runCatching { uriHandler.openUri(PRIVACY_POLICY_URL) } + }, + ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "action_source") { + ActionRow( + icon = Icons.AutoMirrored.Outlined.OpenInNew, + title = "Source code on GitHub", + subtitle = "View this app's source.", + onClick = { + runCatching { uriHandler.openUri(SOURCE_CODE_URL) } + }, + ) + Spacer(Modifier.height(8.dp)) + } + } +} + +@Composable +private fun AppIdentityCard(versionName: String) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(Radii.cardSm) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Store, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp), + ) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = "GitHub Store", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = versionName.ifBlank { "—" }, + style = MaterialTheme.typography.labelLarge.copy( + fontFamily = FontFamily.Monospace, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Cross-platform app store for GitHub, Codeberg, and Forgejo releases.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun ActionRow( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt new file mode 100644 index 000000000..9a6c6394f --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt @@ -0,0 +1,149 @@ +package zed.rainxch.tweaks.presentation.licenses + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +private data class Library( + val name: String, + val license: String, + val url: String, +) + +private val LIBRARIES: List = listOf( + Library("Kotlin", "Apache-2.0", "https://github.com/JetBrains/kotlin"), + Library("Compose Multiplatform", "Apache-2.0", "https://github.com/JetBrains/compose-multiplatform"), + Library("Jetpack Compose", "Apache-2.0", "https://developer.android.com/jetpack/compose"), + Library("Ktor", "Apache-2.0", "https://github.com/ktorio/ktor"), + Library("Room", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/room"), + Library("Koin", "Apache-2.0", "https://github.com/InsertKoinIO/koin"), + Library("kotlinx.serialization", "Apache-2.0", "https://github.com/Kotlin/kotlinx.serialization"), + Library("kotlinx.coroutines", "Apache-2.0", "https://github.com/Kotlin/kotlinx.coroutines"), + Library("kotlinx.datetime", "Apache-2.0", "https://github.com/Kotlin/kotlinx-datetime"), + Library("DataStore", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/datastore"), + Library("Landscapist", "Apache-2.0", "https://github.com/skydoves/landscapist"), + Library("Kermit", "Apache-2.0", "https://github.com/touchlab/Kermit"), + Library("MOKO Permissions", "Apache-2.0", "https://github.com/icerockdev/moko-permissions"), + Library("Navigation Compose", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/navigation"), + Library("multiplatform-markdown-renderer", "Apache-2.0", "https://github.com/mikepenz/multiplatform-markdown-renderer"), + Library("Shizuku", "Apache-2.0", "https://github.com/RikkaApps/Shizuku"), + Library("WorkManager", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/work"), + Library("KSafe", "Apache-2.0", "https://github.com/Anifantakis/KSafe"), + Library("Geist (Vercel)", "OFL-1.1", "https://github.com/vercel/geist-font"), + Library("Material Icons Extended", "Apache-2.0", "https://fonts.google.com/icons"), +) + +@Composable +fun LicensesRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val uriHandler = LocalUriHandler.current + + TweaksSubScreenScaffold( + title = "Open source licenses", + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "intro") { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "GitHub Store stands on these libraries.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Tap any entry to open its project page.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.height(12.dp)) + } + + LIBRARIES.forEach { library -> + item(key = "lib_${library.name}") { + LibraryRow(library = library, onClick = { + runCatching { uriHandler.openUri(library.url) } + }) + Spacer(Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun LibraryRow(library: Library, onClick: () -> Unit) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = library.name, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = library.license, + style = MaterialTheme.typography.labelSmall.copy( + fontFamily = FontFamily.Monospace, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} From 8b9491231941d6e30c24b23fabf7ff3af2a46d7b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:01:14 +0500 Subject: [PATCH 082/172] feat(nav): wire Wave 2 Tweaks sub-screens --- .../app/navigation/AppNavigation.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 093cd947e..1cdc5908e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -73,11 +73,16 @@ import zed.rainxch.search.presentation.mappers.toSearchPlatformUi import zed.rainxch.search.presentation.model.SearchPlatformUi import zed.rainxch.starred.presentation.StarredReposRoot import zed.rainxch.tweaks.presentation.TweaksRoot +import zed.rainxch.tweaks.presentation.appearance.TweaksAppearanceRoot +import zed.rainxch.tweaks.presentation.appinfo.TweaksAppInfoRoot import zed.rainxch.tweaks.presentation.components.TweaksStubScreen import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.hosttokens.HostTokensRoot +import zed.rainxch.tweaks.presentation.language.TweaksLanguageRoot +import zed.rainxch.tweaks.presentation.licenses.LicensesRoot import zed.rainxch.tweaks.presentation.mirror.MirrorPickerRoot import zed.rainxch.tweaks.presentation.skipped.SkippedUpdatesRoot +import zed.rainxch.tweaks.presentation.storage.TweaksStorageRoot @Composable fun AppNavigation( @@ -715,15 +720,13 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_appearance), + TweaksAppearanceRoot( onNavigateBack = { navController.popBackStack() }, ) } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_language), + TweaksLanguageRoot( onNavigateBack = { navController.popBackStack() }, ) } @@ -764,8 +767,7 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_storage), + TweaksStorageRoot( onNavigateBack = { navController.popBackStack() }, ) } @@ -778,8 +780,23 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_app_info), + TweaksAppInfoRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToWhatsNewHistory = { + navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) { + launchSingleTop = true + } + }, + onNavigateToLicenses = { + navController.navigate(GithubStoreGraph.LicensesScreen) { + launchSingleTop = true + } + }, + ) + } + + composable { + LicensesRoot( onNavigateBack = { navController.popBackStack() }, ) } From c0534b6f85775ef1b51353ba30db9f43329e43ad Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:06:59 +0500 Subject: [PATCH 083/172] feat(tweaks): master proxy state actions and handlers --- .../tweaks/presentation/TweaksAction.kt | 11 + .../tweaks/presentation/TweaksState.kt | 5 + .../tweaks/presentation/TweaksViewModel.kt | 223 ++++++++++++++++++ 3 files changed, 239 insertions(+) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index bb6c42c04..c5a1d7036 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -212,4 +212,15 @@ sealed interface TweaksAction { data object OnRestartNowClick : TweaksAction data object OnRestartLaterClick : TweaksAction + + data class OnMasterProxyTypeSelected(val type: ProxyType) : TweaksAction + data class OnMasterProxyHostChanged(val host: String) : TweaksAction + data class OnMasterProxyPortChanged(val port: String) : TweaksAction + data class OnMasterProxyUsernameChanged(val username: String) : TweaksAction + data class OnMasterProxyPasswordChanged(val password: String) : TweaksAction + data object OnMasterProxyPasswordVisibilityToggle : TweaksAction + data object OnMasterProxySave : TweaksAction + data object OnMasterProxyTest : TweaksAction + + data class OnScopeUseMainToggled(val scope: ProxyScope, val useMain: Boolean) : TweaksAction } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 038210a97..8ff6fe64e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -66,11 +66,16 @@ data class TweaksState( val customForgeError: String? = null, val needsRestartReasons: Set = emptySet(), val restartBannerSessionDismissed: Boolean = false, + val masterProxyForm: ProxyScopeFormState = ProxyScopeFormState(), + val useMasterByScope: Map = + ProxyScope.entries.associateWith { false }, ) { val restartBannerVisible: Boolean get() = needsRestartReasons.isNotEmpty() && !restartBannerSessionDismissed + fun useMain(scope: ProxyScope): Boolean = useMasterByScope[scope] ?: false + val displayedTranslationProvider: TranslationProvider get() = draftTranslationProvider ?: translationProvider diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index fe1ed9230..85bd2ff51 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -106,6 +106,8 @@ class TweaksViewModel( observeRootStatus() observeInstallerAttribution() observeNeedsRestartReasons() + observeMasterProxyConfig() + observeUseMasterFlags() hasLoadedInitialData = true } @@ -1187,6 +1189,132 @@ class TweaksViewModel( TweaksAction.OnRestartLaterClick -> { _state.update { it.copy(restartBannerSessionDismissed = true) } } + + is TweaksAction.OnMasterProxyTypeSelected -> { + mutateMasterForm { it.copy(type = action.type) } + } + + is TweaksAction.OnMasterProxyHostChanged -> { + mutateMasterForm { it.copy(host = action.host) } + } + + is TweaksAction.OnMasterProxyPortChanged -> { + mutateMasterForm { it.copy(port = action.port) } + } + + is TweaksAction.OnMasterProxyUsernameChanged -> { + mutateMasterForm { it.copy(username = action.username) } + } + + is TweaksAction.OnMasterProxyPasswordChanged -> { + mutateMasterForm { it.copy(password = action.password) } + } + + TweaksAction.OnMasterProxyPasswordVisibilityToggle -> { + mutateMasterForm { it.copy(isPasswordVisible = !it.isPasswordVisible) } + } + + TweaksAction.OnMasterProxySave -> { + val config = buildMasterProxyConfig() ?: return + viewModelScope.launch { + runCatching { + proxyRepository.setMasterProxyConfig(config) + ProxyScope.entries.forEach { scope -> + if (_state.value.useMain(scope)) { + proxyRepository.setProxyConfig(scope, config) + } + } + }.onSuccess { + _state.update { + it.copy( + masterProxyForm = it.masterProxyForm.copy(isDraftDirty = false), + ) + } + _events.send(TweaksEvent.OnProxySaved) + }.onFailure { error -> + _events.send( + TweaksEvent.OnProxySaveError( + error.message + ?: getString(Res.string.failed_to_save_proxy_settings), + ), + ) + } + } + } + + TweaksAction.OnMasterProxyTest -> { + val form = _state.value.masterProxyForm + if (form.isTestInProgress) return + val config = buildMasterProxyConfigForTest() ?: return + _state.update { + it.copy(masterProxyForm = it.masterProxyForm.copy(isTestInProgress = true)) + } + viewModelScope.launch { + val outcome: ProxyTestOutcome = try { + proxyTester.test(config) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + ProxyTestOutcome.Failure.Unknown(e.message) + } finally { + _state.update { + it.copy( + masterProxyForm = it.masterProxyForm.copy(isTestInProgress = false), + ) + } + } + _events.send(outcome.toEvent()) + } + } + + is TweaksAction.OnScopeUseMainToggled -> { + viewModelScope.launch { + runCatching { + proxyRepository.setUseMaster(action.scope, action.useMain) + if (action.useMain) { + val master = proxyRepository.getMasterProxyConfig().first() + if (master != null) { + proxyRepository.setProxyConfig(action.scope, master) + } + } + }.onFailure { error -> + _events.send( + TweaksEvent.OnProxySaveError( + error.message + ?: getString(Res.string.failed_to_save_proxy_settings), + ), + ) + } + } + } + } + } + + private fun buildMasterProxyConfigForTest(): ProxyConfig? { + val form = _state.value.masterProxyForm + return when (form.type) { + ProxyType.NONE -> ProxyConfig.None + ProxyType.SYSTEM -> ProxyConfig.System + ProxyType.HTTP -> { + val host = form.host.trim().takeIf { it.isNotEmpty() } ?: return null + val port = form.port.toIntOrNull() ?: return null + ProxyConfig.Http( + host, + port, + form.username.takeIf { it.isNotBlank() }, + form.password.takeIf { it.isNotBlank() }, + ) + } + ProxyType.SOCKS -> { + val host = form.host.trim().takeIf { it.isNotEmpty() } ?: return null + val port = form.port.toIntOrNull() ?: return null + ProxyConfig.Socks( + host, + port, + form.username.takeIf { it.isNotBlank() }, + form.password.takeIf { it.isNotBlank() }, + ) + } } } @@ -1198,6 +1326,101 @@ class TweaksViewModel( } } + private fun observeMasterProxyConfig() { + viewModelScope.launch { + proxyRepository.getMasterProxyConfig().collect { config -> + _state.update { state -> + if (state.masterProxyForm.isDraftDirty) return@update state + val existing = state.masterProxyForm + val populated = when (config) { + null -> existing.copy(type = ProxyType.NONE) + is ProxyConfig.None -> existing.copy( + type = ProxyType.NONE, + ) + is ProxyConfig.System -> existing.copy( + type = ProxyType.SYSTEM, + ) + is ProxyConfig.Http -> existing.copy( + type = ProxyType.HTTP, + host = config.host, + port = config.port.toString(), + username = config.username.orEmpty(), + password = config.password.orEmpty(), + ) + is ProxyConfig.Socks -> existing.copy( + type = ProxyType.SOCKS, + host = config.host, + port = config.port.toString(), + username = config.username.orEmpty(), + password = config.password.orEmpty(), + ) + } + state.copy(masterProxyForm = populated) + } + } + } + } + + private fun observeUseMasterFlags() { + ProxyScope.entries.forEach { scope -> + viewModelScope.launch { + proxyRepository.getUseMaster(scope).collect { useMaster -> + _state.update { state -> + state.copy( + useMasterByScope = state.useMasterByScope + (scope to useMaster), + ) + } + } + } + } + } + + private fun mutateMasterForm(block: (ProxyScopeFormState) -> ProxyScopeFormState) { + _state.update { state -> + val updated = block(state.masterProxyForm).copy(isDraftDirty = true) + state.copy(masterProxyForm = updated) + } + } + + private fun buildMasterProxyConfig(): ProxyConfig? { + val form = _state.value.masterProxyForm + return when (form.type) { + ProxyType.NONE -> ProxyConfig.None + ProxyType.SYSTEM -> ProxyConfig.System + ProxyType.HTTP, ProxyType.SOCKS -> { + val port = form.port.toIntOrNull()?.takeIf { it in 1..65535 } ?: run { + viewModelScope.launch { + _events.send( + TweaksEvent.OnProxySaveError( + getString(Res.string.invalid_proxy_port), + ), + ) + } + return null + } + val host = form.host.trim().takeIf { isValidProxyHost(it) } ?: run { + val isBlank = form.host.isBlank() + viewModelScope.launch { + val msg = if (isBlank) { + getString(Res.string.proxy_host_required) + } else { + getString(Res.string.proxy_host_invalid) + } + _events.send(TweaksEvent.OnProxySaveError(msg)) + } + return null + } + val username = form.username.takeIf { it.isNotBlank() } + val password = form.password.takeIf { it.isNotBlank() } + if (form.type == ProxyType.HTTP) { + ProxyConfig.Http(host, port, username, password) + } else { + ProxyConfig.Socks(host, port, username, password) + } + } + } + } + private fun buildProxyConfigForTest(scope: ProxyScope): ProxyConfig? { val form = _state.value.formFor(scope) return when (form.type) { From 061b5ce58424e77181ef294ebad4f69e72f52824 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:07:00 +0500 Subject: [PATCH 084/172] feat(tweaks): Connection sub-screen with main proxy and per-scope overrides --- .../connection/TweaksConnectionRoot.kt | 585 ++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt new file mode 100644 index 000000000..0621a1e05 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -0,0 +1,585 @@ +package zed.rainxch.tweaks.presentation.connection + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.NetworkCheck +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.model.ProxyScope +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.proxy_password +import zed.rainxch.githubstore.core.presentation.res.proxy_port +import zed.rainxch.githubstore.core.presentation.res.proxy_save +import zed.rainxch.githubstore.core.presentation.res.proxy_saved +import zed.rainxch.githubstore.core.presentation.res.proxy_host +import zed.rainxch.githubstore.core.presentation.res.proxy_test_success +import zed.rainxch.githubstore.core.presentation.res.proxy_username +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_connection +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksEvent +import zed.rainxch.tweaks.presentation.TweaksState +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold +import zed.rainxch.tweaks.presentation.model.ProxyScopeFormState +import zed.rainxch.tweaks.presentation.model.ProxyType + +@Composable +fun TweaksConnectionRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + ObserveAsEvents(viewModel.events) { event -> + when (event) { + TweaksEvent.OnProxySaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.proxy_saved)) + } + is TweaksEvent.OnProxySaveError -> coroutineScope.launch { + snackbarState.showSnackbar(event.message) + } + is TweaksEvent.OnProxyTestSuccess -> coroutineScope.launch { + snackbarState.showSnackbar( + getString(Res.string.proxy_test_success, event.latencyMs), + ) + } + is TweaksEvent.OnProxyTestError -> coroutineScope.launch { + snackbarState.showSnackbar(event.message) + } + else -> Unit + } + } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_connection), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "intro") { + IntroCard() + Spacer(Modifier.height(16.dp)) + } + + item(key = "main_card") { + MainConnectionCard( + form = state.masterProxyForm, + onAction = { viewModel.onAction(it) }, + ) + Spacer(Modifier.height(16.dp)) + } + + item(key = "overrides_card") { + OverridesCard( + state = state, + onAction = { viewModel.onAction(it) }, + ) + } + } +} + +@Composable +private fun IntroCard() { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "How the app reaches the internet", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Pick a connection mode below. Most people leave this on No proxy.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun MainConnectionCard( + form: ProxyScopeFormState, + onAction: (TweaksAction) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Main connection", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(12.dp)) + + ModePillSegment( + selected = form.type, + onSelected = { onAction(TweaksAction.OnMasterProxyTypeSelected(it)) }, + ) + + AnimatedVisibility(visible = form.type == ProxyType.HTTP || form.type == ProxyType.SOCKS) { + Column { + Spacer(Modifier.height(12.dp)) + ProxyFormFields( + form = form, + onHostChange = { onAction(TweaksAction.OnMasterProxyHostChanged(it)) }, + onPortChange = { onAction(TweaksAction.OnMasterProxyPortChanged(it)) }, + onUserChange = { onAction(TweaksAction.OnMasterProxyUsernameChanged(it)) }, + onPassChange = { onAction(TweaksAction.OnMasterProxyPasswordChanged(it)) }, + onPassVisibility = { onAction(TweaksAction.OnMasterProxyPasswordVisibilityToggle) }, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Applies to all traffic unless overridden below.", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedButton( + onClick = { onAction(TweaksAction.OnMasterProxyTest) }, + shape = Radii.chip, + modifier = Modifier.weight(1f), + enabled = !form.isTestInProgress, + ) { + if (form.isTestInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + } else { + Icon( + imageVector = Icons.Default.NetworkCheck, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.size(6.dp)) + Text(text = "Test main connection") + } + FilledTonalButton( + onClick = { onAction(TweaksAction.OnMasterProxySave) }, + shape = WonkySquircleShape.CtaPrimary, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(6.dp)) + Text(text = stringResource(Res.string.proxy_save)) + } + } + } + } + + AnimatedVisibility(visible = form.type == ProxyType.NONE || form.type == ProxyType.SYSTEM) { + Column { + Spacer(Modifier.height(12.dp)) + FilledTonalButton( + onClick = { onAction(TweaksAction.OnMasterProxySave) }, + shape = WonkySquircleShape.CtaPrimary, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(6.dp)) + Text(text = stringResource(Res.string.proxy_save)) + } + } + } + } + } +} + +@Composable +private fun ProxyFormFields( + form: ProxyScopeFormState, + onHostChange: (String) -> Unit, + onPortChange: (String) -> Unit, + onUserChange: (String) -> Unit, + onPassChange: (String) -> Unit, + onPassVisibility: () -> Unit, +) { + val colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.outline, + unfocusedBorderColor = MaterialTheme.colorScheme.outline, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = form.host, + onValueChange = onHostChange, + modifier = Modifier.weight(2f), + label = { Text(text = stringResource(Res.string.proxy_host)) }, + singleLine = true, + shape = Radii.chip, + colors = colors, + ) + OutlinedTextField( + value = form.port, + onValueChange = onPortChange, + modifier = Modifier.weight(1f), + label = { Text(text = stringResource(Res.string.proxy_port)) }, + singleLine = true, + shape = Radii.chip, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + colors = colors, + ) + } + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = form.username, + onValueChange = onUserChange, + modifier = Modifier.fillMaxWidth(), + label = { Text(text = stringResource(Res.string.proxy_username)) }, + singleLine = true, + shape = Radii.chip, + colors = colors, + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = form.password, + onValueChange = onPassChange, + modifier = Modifier.fillMaxWidth(), + label = { Text(text = stringResource(Res.string.proxy_password)) }, + singleLine = true, + visualTransformation = if (form.isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = onPassVisibility) { + Icon( + imageVector = if (form.isPasswordVisible) { + Icons.Filled.VisibilityOff + } else { + Icons.Filled.Visibility + }, + contentDescription = null, + ) + } + }, + shape = Radii.chip, + colors = colors, + ) +} + +@Composable +private fun ModePillSegment( + selected: ProxyType, + onSelected: (ProxyType) -> Unit, +) { + val items = listOf( + ProxyType.NONE to ("No proxy" to null), + ProxyType.SYSTEM to ("System" to null), + ProxyType.HTTP to ("HTTP/HTTPS" to "Most corporate proxies."), + ProxyType.SOCKS to ("SOCKS5" to "Tor, SSH tunnels."), + ) + + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + items.forEach { (type, labels) -> + val isSelected = type == selected + val container = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + } + val content = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + Box( + modifier = Modifier + .weight(1f) + .height(40.dp) + .clip(RoundedCornerShape(10.dp)) + .background(container) + .clickable { onSelected(type) }, + contentAlignment = Alignment.Center, + ) { + Text( + text = labels.first, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + ) + } + } + } + val caption = items.firstOrNull { it.first == selected }?.second?.second + if (!caption.isNullOrBlank()) { + Text( + text = caption, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp), + ) + } + } +} + +@Composable +private fun OverridesCard( + state: TweaksState, + onAction: (TweaksAction) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Per-scope overrides", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "Each scope uses the main connection by default. Choose 'Custom' to keep its own settings.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(12.dp)) + + ProxyScope.entries.forEachIndexed { idx, scope -> + if (idx > 0) Spacer(Modifier.height(8.dp)) + ScopeOverrideRow( + scope = scope, + useMain = state.useMain(scope), + scopeForm = state.formFor(scope), + onToggle = { useMain -> + onAction(TweaksAction.OnScopeUseMainToggled(scope, useMain)) + }, + ) + } + } + } +} + +@Composable +private fun ScopeOverrideRow( + scope: ProxyScope, + useMain: Boolean, + scopeForm: ProxyScopeFormState, + onToggle: (Boolean) -> Unit, +) { + val (title, subtitle) = when (scope) { + ProxyScope.DISCOVERY -> "Search & metadata" to "GitHub API, search, repo details." + ProxyScope.DOWNLOAD -> "Downloads" to "APK and asset downloads." + ProxyScope.TRANSLATION -> "Translation" to "DeepL, Microsoft, LibreTranslate calls." + } + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.chip, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column(modifier = Modifier.padding(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + UseMainSegment( + useMain = useMain, + onSelected = onToggle, + ) + } + + AnimatedVisibility(visible = !useMain) { + Column { + Spacer(Modifier.height(8.dp)) + Text( + text = scopeStatusLabel(scopeForm), + style = MaterialTheme.typography.labelSmall.copy( + fontFamily = FontFamily.Monospace, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} + +private fun scopeStatusLabel(form: ProxyScopeFormState): String = + when (form.type) { + ProxyType.NONE -> "Custom: no proxy" + ProxyType.SYSTEM -> "Custom: system" + ProxyType.HTTP -> "Custom: HTTP ${form.host.ifBlank { "—" }}:${form.port.ifBlank { "—" }}" + ProxyType.SOCKS -> "Custom: SOCKS5 ${form.host.ifBlank { "—" }}:${form.port.ifBlank { "—" }}" + } + +@Composable +private fun UseMainSegment( + useMain: Boolean, + onSelected: (Boolean) -> Unit, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .padding(3.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + SegmentChip( + label = "Use main", + selected = useMain, + onClick = { onSelected(true) }, + ) + SegmentChip( + label = "Custom", + selected = !useMain, + onClick = { onSelected(false) }, + ) + } +} + +@Composable +private fun SegmentChip( + label: String, + selected: Boolean, + onClick: () -> Unit, +) { + val container = if (selected) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + } + val content = if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + Box( + modifier = Modifier + .clip(RoundedCornerShape(9.dp)) + .background(container) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + ) + } +} From 1818e131edeb324620f9f55afac81c0dca5f65cf Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:07:01 +0500 Subject: [PATCH 085/172] feat(nav): wire Connection sub-screen --- .../zed/rainxch/githubstore/app/navigation/AppNavigation.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 1cdc5908e..273b41f5b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -76,6 +76,7 @@ import zed.rainxch.tweaks.presentation.TweaksRoot import zed.rainxch.tweaks.presentation.appearance.TweaksAppearanceRoot import zed.rainxch.tweaks.presentation.appinfo.TweaksAppInfoRoot import zed.rainxch.tweaks.presentation.components.TweaksStubScreen +import zed.rainxch.tweaks.presentation.connection.TweaksConnectionRoot import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.hosttokens.HostTokensRoot import zed.rainxch.tweaks.presentation.language.TweaksLanguageRoot @@ -732,8 +733,7 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_connection), + TweaksConnectionRoot( onNavigateBack = { navController.popBackStack() }, ) } From eb1518316cedbf4ac4c9abb69a85083418129f30 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:43 +0500 Subject: [PATCH 086/172] feat(core): telemetry opt-out preference --- .../rainxch/core/data/repository/TweaksRepositoryImpl.kt | 9 +++++++++ .../rainxch/core/domain/repository/TweaksRepository.kt | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index 67d418322..c679756bb 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -488,6 +488,14 @@ class TweaksRepositoryImpl( } } + override fun getTelemetryEnabled(): Flow = + gatedGetFlow(K_TELEMETRY_ENABLED, false) + + override suspend fun setTelemetryEnabled(enabled: Boolean) { + migrationDeferred.await() + ksafe.safePut(K_TELEMETRY_ENABLED, enabled) + } + companion object { private const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L private const val MIGRATION_MARKER = "__migrated_from_datastore_v1__" @@ -537,5 +545,6 @@ class TweaksRepositoryImpl( private const val K_CONTENT_WIDTH = "content_width" private const val K_CUSTOM_FORGE_HOSTS = "custom_forge_hosts" private const val K_RESTART_REASONS = "needs_restart_reasons" + private const val K_TELEMETRY_ENABLED = "telemetry_enabled" } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index d984b24ae..2ef4cbda2 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -199,4 +199,8 @@ interface TweaksRepository { suspend fun addRestartReason(reason: RestartReason) suspend fun clearRestartReasons() + + fun getTelemetryEnabled(): Flow + + suspend fun setTelemetryEnabled(enabled: Boolean) } From 38658e41f5a8b0dcfa91707b47a45051868eb3e8 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:45 +0500 Subject: [PATCH 087/172] feat(tweaks): privacy state actions and handlers --- .../tweaks/presentation/TweaksAction.kt | 6 +++ .../tweaks/presentation/TweaksState.kt | 3 ++ .../tweaks/presentation/TweaksViewModel.kt | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index c5a1d7036..99c4de167 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -223,4 +223,10 @@ sealed interface TweaksAction { data object OnMasterProxyTest : TweaksAction data class OnScopeUseMainToggled(val scope: ProxyScope, val useMain: Boolean) : TweaksAction + + data class OnTelemetryToggled(val enabled: Boolean) : TweaksAction + data object OnTelemetryExpandToggle : TweaksAction + data object OnClearSeenHistoryRequest : TweaksAction + data object OnClearSeenHistoryDismiss : TweaksAction + data object OnClearSeenHistoryConfirm : TweaksAction } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 8ff6fe64e..cf28f7c27 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -69,6 +69,9 @@ data class TweaksState( val masterProxyForm: ProxyScopeFormState = ProxyScopeFormState(), val useMasterByScope: Map = ProxyScope.entries.associateWith { false }, + val telemetryEnabled: Boolean = false, + val isClearSeenHistoryDialogVisible: Boolean = false, + val telemetryExpanded: Boolean = false, ) { val restartBannerVisible: Boolean diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 85bd2ff51..2153546d4 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -108,6 +108,7 @@ class TweaksViewModel( observeNeedsRestartReasons() observeMasterProxyConfig() observeUseMasterFlags() + observeTelemetryEnabled() hasLoadedInitialData = true } @@ -1267,6 +1268,34 @@ class TweaksViewModel( } } + is TweaksAction.OnTelemetryToggled -> { + viewModelScope.launch { + runCatching { tweaksRepository.setTelemetryEnabled(action.enabled) } + runCatching { + tweaksRepository.addRestartReason( + zed.rainxch.core.domain.model.RestartReason.TELEMETRY_TOGGLE, + ) + } + } + } + + TweaksAction.OnTelemetryExpandToggle -> { + _state.update { it.copy(telemetryExpanded = !it.telemetryExpanded) } + } + + TweaksAction.OnClearSeenHistoryRequest -> { + _state.update { it.copy(isClearSeenHistoryDialogVisible = true) } + } + + TweaksAction.OnClearSeenHistoryDismiss -> { + _state.update { it.copy(isClearSeenHistoryDialogVisible = false) } + } + + TweaksAction.OnClearSeenHistoryConfirm -> { + _state.update { it.copy(isClearSeenHistoryDialogVisible = false) } + onAction(TweaksAction.OnClearSeenRepos) + } + is TweaksAction.OnScopeUseMainToggled -> { viewModelScope.launch { runCatching { @@ -1375,6 +1404,14 @@ class TweaksViewModel( } } + private fun observeTelemetryEnabled() { + viewModelScope.launch { + tweaksRepository.getTelemetryEnabled().collect { enabled -> + _state.update { it.copy(telemetryEnabled = enabled) } + } + } + } + private fun mutateMasterForm(block: (ProxyScopeFormState) -> ProxyScopeFormState) { _state.update { state -> val updated = block(state.masterProxyForm).copy(isDraftDirty = true) From a56276d09370608cc9d20c6589bc9e706a635443 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:46 +0500 Subject: [PATCH 088/172] feat(tweaks): Privacy sub-screen with telemetry opt-out --- .../presentation/privacy/TweaksPrivacyRoot.kt | 307 ++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt new file mode 100644 index 000000000..58bd24fea --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -0,0 +1,307 @@ +package zed.rainxch.tweaks.presentation.privacy + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.components.overlays.GhsConfirmDialog +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_privacy +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +@Composable +fun TweaksPrivacyRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_privacy), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "telemetry_card") { + TelemetryCard( + enabled = state.telemetryEnabled, + expanded = state.telemetryExpanded, + onToggled = { viewModel.onAction(TweaksAction.OnTelemetryToggled(it)) }, + onExpandToggle = { viewModel.onAction(TweaksAction.OnTelemetryExpandToggle) }, + ) + Spacer(Modifier.height(12.dp)) + } + + item(key = "clipboard_card") { + ToggleCard( + title = "Detect repo links in clipboard", + subtitle = "When you copy a github.com or codeberg.org link, we'll prompt to open it.", + checked = state.autoDetectClipboardLinks, + onCheckedChange = { + viewModel.onAction(TweaksAction.OnAutoDetectClipboardToggled(it)) + }, + ) + Spacer(Modifier.height(12.dp)) + } + + item(key = "history_header") { + Text( + text = "Browsing history", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp, top = 4.dp, bottom = 8.dp), + ) + } + + item(key = "hide_seen_card") { + ToggleCard( + title = "Hide repos I've already viewed", + subtitle = "Skip seen repos in feeds and search.", + checked = state.isHideSeenEnabled, + onCheckedChange = { viewModel.onAction(TweaksAction.OnHideSeenToggled(it)) }, + ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "clear_history_row") { + DestructiveRow( + title = "Clear viewed history", + subtitle = "Forget which repos you've already opened.", + onClick = { viewModel.onAction(TweaksAction.OnClearSeenHistoryRequest) }, + ) + } + } + + if (state.isClearSeenHistoryDialogVisible) { + GhsConfirmDialog( + title = "Clear viewed history?", + body = "This won't unstar or unfavorite anything.", + confirmLabel = "Clear", + destructive = true, + onConfirm = { viewModel.onAction(TweaksAction.OnClearSeenHistoryConfirm) }, + onDismiss = { viewModel.onAction(TweaksAction.OnClearSeenHistoryDismiss) }, + ) + } +} + +@Composable +private fun TelemetryCard( + enabled: Boolean, + expanded: Boolean, + onToggled: (Boolean) -> Unit, + onExpandToggle: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Usage data", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(12.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.chip) + .clickable { onToggled(!enabled) } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Share anonymous usage data", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "Help us understand which features get used.", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = enabled, + onCheckedChange = null, + ) + } + + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.chip) + .clickable(onClick = onExpandToggle) + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "What we collect", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.primary, + ) + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .size(20.dp) + .rotate(if (expanded) 180f else 0f), + ) + } + + AnimatedVisibility(visible = expanded) { + Column( + modifier = Modifier.padding(top = 4.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + listOf( + "App version.", + "OS and platform.", + "Feature usage counts.", + "No repo names.", + "No tokens.", + "No identifiers.", + ).forEach { line -> + Text( + text = "• $line", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun ToggleCard( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable { onCheckedChange(!checked) }, + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Switch( + checked = checked, + onCheckedChange = null, + ) + } + } +} + +@Composable +private fun DestructiveRow( + title: String, + subtitle: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.error, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} From 80c51587c6c904fa471e1727dc72e9528aea6b65 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:48 +0500 Subject: [PATCH 089/172] feat(tweaks): Update behavior sub-screen --- .../presentation/updates/TweaksUpdatesRoot.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt new file mode 100644 index 000000000..df5eea2e6 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt @@ -0,0 +1,47 @@ +package zed.rainxch.tweaks.presentation.updates + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_updates +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold +import zed.rainxch.tweaks.presentation.components.sections.updatesSection + +@Composable +fun TweaksUpdatesRoot( + onNavigateBack: () -> Unit, + onNavigateToSkippedUpdates: () -> Unit, + onNavigateToHiddenRepositories: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_updates), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + updatesSection( + state = state, + onAction = { action -> + when (action) { + TweaksAction.OnSkippedUpdatesClick -> onNavigateToSkippedUpdates() + TweaksAction.OnHiddenRepositoriesClick -> onNavigateToHiddenRepositories() + else -> viewModel.onAction(action) + } + }, + ) + } +} From c1c1a4c65e18e2217ef6d4ac45af2b63350f16dc Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:49 +0500 Subject: [PATCH 090/172] feat(tweaks): Sources sub-screen for mirror and custom forges --- .../presentation/sources/TweaksSourcesRoot.kt | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt new file mode 100644 index 000000000..d606c9e98 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -0,0 +1,253 @@ +package zed.rainxch.tweaks.presentation.sources + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Dns +import androidx.compose.material.icons.outlined.NetworkCheck +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_sources +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.CustomForgesDialog +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold + +@Composable +fun TweaksSourcesRoot( + onNavigateBack: () -> Unit, + onNavigateToMirrorPicker: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_sources), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + item(key = "intro") { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Where the app looks for repositories", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = "The app searches GitHub by default. Route through a regional mirror or add custom Forgejo / Gitea hosts.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.height(16.dp)) + } + + item(key = "mirror_row") { + DrillRow( + icon = Icons.Outlined.NetworkCheck, + title = "GitHub mirror", + subtitle = "Default (github.com)", + onClick = onNavigateToMirrorPicker, + ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "custom_forges_row") { + val count = state.customForgeHosts.size + DrillRow( + icon = Icons.Outlined.Dns, + title = "Custom forges", + subtitle = if (count == 0) { + "Add a Forgejo or Gitea host" + } else { + "$count host${if (count == 1) "" else "s"}" + }, + onClick = { viewModel.onAction(TweaksAction.OnOpenCustomForgesDialog) }, + ) + } + + if (state.customForgeHosts.isNotEmpty()) { + item(key = "forges_subheader") { + Spacer(Modifier.height(16.dp)) + Text( + text = "Added hosts", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp), + ) + } + + state.customForgeHosts.sorted().forEach { host -> + item(key = "forge_$host") { + ForgeHostRow( + host = host, + onRemove = { viewModel.onAction(TweaksAction.OnRemoveCustomForge(host)) }, + ) + Spacer(Modifier.height(8.dp)) + } + } + } + } + + if (state.showCustomForgesDialog) { + CustomForgesDialog( + state = state, + onAction = { viewModel.onAction(it) }, + ) + } +} + +@Composable +private fun DrillRow( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } + } +} + +@Composable +private fun ForgeHostRow( + host: String, + onRemove: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = host, + style = MaterialTheme.typography.labelLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + IconButton( + onClick = onRemove, + modifier = Modifier.size(40.dp).clip(Radii.chip), + ) { + Icon( + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = "Remove", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp), + ) + } + } + } +} From 6ce9d12229b36b58f5bd68a5e355bffa02a053e4 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:50 +0500 Subject: [PATCH 091/172] feat(tweaks): Translation sub-screen --- .../translation/TweaksTranslationRoot.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/translation/TweaksTranslationRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/translation/TweaksTranslationRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/translation/TweaksTranslationRoot.kt new file mode 100644 index 000000000..448182280 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/translation/TweaksTranslationRoot.kt @@ -0,0 +1,71 @@ +package zed.rainxch.tweaks.presentation.translation + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.translation_deepl_saved +import zed.rainxch.githubstore.core.presentation.res.translation_libre_saved +import zed.rainxch.githubstore.core.presentation.res.translation_microsoft_saved +import zed.rainxch.githubstore.core.presentation.res.translation_provider_saved +import zed.rainxch.githubstore.core.presentation.res.translation_youdao_saved +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_translation +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksEvent +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold +import zed.rainxch.tweaks.presentation.components.sections.translationSection + +@Composable +fun TweaksTranslationRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + ObserveAsEvents(viewModel.events) { event -> + when (event) { + TweaksEvent.OnTranslationProviderSaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.translation_provider_saved)) + } + TweaksEvent.OnYoudaoCredentialsSaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.translation_youdao_saved)) + } + TweaksEvent.OnLibreTranslateCredentialsSaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.translation_libre_saved)) + } + TweaksEvent.OnDeeplCredentialsSaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.translation_deepl_saved)) + } + TweaksEvent.OnMicrosoftTranslatorCredentialsSaved -> coroutineScope.launch { + snackbarState.showSnackbar(getString(Res.string.translation_microsoft_saved)) + } + else -> Unit + } + } + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_translation), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + translationSection( + state = state, + onAction = { viewModel.onAction(it) }, + ) + } +} From 04222d28ecc9b8291467115705a8843703254d1a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:51 +0500 Subject: [PATCH 092/172] feat(tweaks): Install method sub-screen with desktop empty state --- .../presentation/install/TweaksInstallRoot.kt | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/install/TweaksInstallRoot.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/install/TweaksInstallRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/install/TweaksInstallRoot.kt new file mode 100644 index 000000000..1a114ec5e --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/install/TweaksInstallRoot.kt @@ -0,0 +1,102 @@ +package zed.rainxch.tweaks.presentation.install + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Computer +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.getPlatform +import zed.rainxch.core.domain.model.Platform +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_install_method +import zed.rainxch.githubstore.core.presentation.res.tweaks_install_method_desktop_empty_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_install_method_desktop_empty_title +import zed.rainxch.tweaks.presentation.TweaksAction +import zed.rainxch.tweaks.presentation.TweaksViewModel +import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold +import zed.rainxch.tweaks.presentation.components.sections.installationSection + +@Composable +fun TweaksInstallRoot( + onNavigateBack: () -> Unit, + viewModel: TweaksViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val isAndroid = getPlatform() == Platform.ANDROID + + TweaksSubScreenScaffold( + title = stringResource(Res.string.tweaks_entry_install_method), + onNavigateBack = onNavigateBack, + snackbarState = snackbarState, + restartReasons = state.needsRestartReasons, + onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, + onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, + showRestartBanner = state.restartBannerVisible, + ) { + if (isAndroid) { + installationSection( + state = state, + onAction = { viewModel.onAction(it) }, + ) + } else { + item(key = "desktop_empty") { + DesktopEmptyState() + } + } + } +} + +@Composable +private fun DesktopEmptyState() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp, vertical = 64.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + imageVector = Icons.Outlined.Computer, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(48.dp), + ) + Text( + text = stringResource(Res.string.tweaks_install_method_desktop_empty_title), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(Res.string.tweaks_install_method_desktop_empty_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } +} From c6f768f62eec1e9c19249ff3e5132c621a0ec648 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:12:52 +0500 Subject: [PATCH 093/172] feat(nav): wire Wave 4 Tweaks sub-screens --- .../app/navigation/AppNavigation.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 273b41f5b..391a692dc 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -79,11 +79,16 @@ import zed.rainxch.tweaks.presentation.components.TweaksStubScreen import zed.rainxch.tweaks.presentation.connection.TweaksConnectionRoot import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.hosttokens.HostTokensRoot +import zed.rainxch.tweaks.presentation.install.TweaksInstallRoot import zed.rainxch.tweaks.presentation.language.TweaksLanguageRoot import zed.rainxch.tweaks.presentation.licenses.LicensesRoot import zed.rainxch.tweaks.presentation.mirror.MirrorPickerRoot +import zed.rainxch.tweaks.presentation.privacy.TweaksPrivacyRoot import zed.rainxch.tweaks.presentation.skipped.SkippedUpdatesRoot +import zed.rainxch.tweaks.presentation.sources.TweaksSourcesRoot import zed.rainxch.tweaks.presentation.storage.TweaksStorageRoot +import zed.rainxch.tweaks.presentation.translation.TweaksTranslationRoot +import zed.rainxch.tweaks.presentation.updates.TweaksUpdatesRoot @Composable fun AppNavigation( @@ -739,30 +744,41 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_sources), + TweaksSourcesRoot( onNavigateBack = { navController.popBackStack() }, + onNavigateToMirrorPicker = { + navController.navigate(GithubStoreGraph.MirrorPickerScreen) { + launchSingleTop = true + } + }, ) } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_translation), + TweaksTranslationRoot( onNavigateBack = { navController.popBackStack() }, ) } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_install_method), + TweaksInstallRoot( onNavigateBack = { navController.popBackStack() }, ) } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_updates), + TweaksUpdatesRoot( onNavigateBack = { navController.popBackStack() }, + onNavigateToSkippedUpdates = { + navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { + launchSingleTop = true + } + }, + onNavigateToHiddenRepositories = { + navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + launchSingleTop = true + } + }, ) } @@ -773,8 +789,7 @@ fun AppNavigation( } composable { - TweaksStubScreen( - title = stringResource(Res.string.tweaks_entry_privacy), + TweaksPrivacyRoot( onNavigateBack = { navController.popBackStack() }, ) } From dc43479ece8ff505d61acd7687f6a28bee7f3226 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:17:30 +0500 Subject: [PATCH 094/172] chore(tweaks): retire dead section files --- .../presentation/components/sections/About.kt | 146 ------ .../components/sections/Language.kt | 208 -------- .../components/sections/Network.kt | 495 ------------------ .../components/sections/Others.kt | 180 ------- .../components/sections/SettingsSection.kt | 60 --- 5 files changed, 1089 deletions(-) delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/About.kt delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt delete mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/SettingsSection.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/About.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/About.kt deleted file mode 100644 index 181a45cc5..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/About.kt +++ /dev/null @@ -1,146 +0,0 @@ -package zed.rainxch.tweaks.presentation.components.sections - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material.icons.filled.Feedback -import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.tweaks.presentation.TweaksAction -import zed.rainxch.tweaks.presentation.components.SectionHeader - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun LazyListScope.about( - versionName: String, - onAction: (TweaksAction) -> Unit, -) { - item { - SectionHeader( - text = stringResource(Res.string.section_about), - ) - - Spacer(Modifier.height(8.dp)) - - ElevatedCard( - modifier = - Modifier - .fillMaxWidth() - .padding(4.dp), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ), - shape = RoundedCornerShape(32.dp), - ) { - AboutItem( - icon = Icons.Filled.Info, - title = stringResource(Res.string.version), - actions = { - Text( - text = versionName, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.outline, - modifier = Modifier.padding(horizontal = 8.dp), - ) - }, - ) - - HorizontalDivider() - - AboutItem( - icon = Icons.Default.Feedback, - title = stringResource(Res.string.feedback_send), - actions = { - IconButton( - shape = IconButtonDefaults.shapes().shape, - onClick = { - onAction(TweaksAction.OnFeedbackClick) - }, - colors = - IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } - }, - ) - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun AboutItem( - icon: ImageVector, - title: String, - actions: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = - Modifier - .size(40.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondaryContainer), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.size(24.dp), - ) - } - - Text( - text = title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - modifier = Modifier.weight(1f), - ) - - actions.invoke() - } -} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt deleted file mode 100644 index 494a0b054..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Language.kt +++ /dev/null @@ -1,208 +0,0 @@ -package zed.rainxch.tweaks.presentation.components.sections - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.domain.model.AppLanguages -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.tweaks.presentation.TweaksAction -import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader - -fun LazyListScope.languageSection( - state: TweaksState, - onAction: (TweaksAction) -> Unit, -) { - item { - SectionHeader(text = stringResource(Res.string.section_language)) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(Res.string.language_intro), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - ) - Spacer(Modifier.height(8.dp)) - - LanguagePickerCard( - state = state, - onAction = onAction, - ) - } -} - -@Composable -private fun LanguagePickerCard( - state: TweaksState, - onAction: (TweaksAction) -> Unit, -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = RoundedCornerShape(32.dp), - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(Res.string.language_picker_title), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = stringResource(Res.string.language_picker_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp), - ) - - Spacer(Modifier.height(12.dp)) - - LanguageDropdown( - selectedTag = state.selectedAppLanguage, - onLanguageSelected = { tag -> - onAction(TweaksAction.OnAppLanguageSelected(tag)) - }, - ) - } - } -} - -@Composable -internal fun LanguageDropdown( - selectedTag: String?, - onLanguageSelected: (String?) -> Unit, -) { - var expanded by remember { mutableStateOf(false) } - - val currentLabel = - when (val match = AppLanguages.findByTag(selectedTag)) { - null -> stringResource(Res.string.language_follow_system) - else -> match.displayName - } - - Box(modifier = Modifier.fillMaxWidth()) { - - Row( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surface) - .clickable { expanded = true } - .padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = currentLabel, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Medium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) - Spacer(Modifier.size(8.dp)) - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - shape = RoundedCornerShape(20.dp), - - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - - DropdownMenuItem( - text = { DropdownItemText(stringResource(Res.string.language_follow_system)) }, - onClick = { - onLanguageSelected(null) - expanded = false - }, - trailingIcon = { - if (selectedTag == null) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - } - }, - ) - - AppLanguages.ALL.forEach { language -> - DropdownMenuItem( - text = { - - DropdownItemText(language.displayName) - }, - onClick = { - onLanguageSelected(language.tag) - expanded = false - }, - trailingIcon = { - if (selectedTag == language.tag) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - } - }, - ) - } - } - } -} - -@Composable -private fun DropdownItemText(label: String) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) -} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt deleted file mode 100644 index c9ed13c4e..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt +++ /dev/null @@ -1,495 +0,0 @@ -package zed.rainxch.tweaks.presentation.components.sections - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material.icons.filled.Dns -import androidx.compose.material.icons.filled.NetworkCheck -import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.domain.model.ProxyScope -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.tweaks.presentation.TweaksAction -import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader -import zed.rainxch.tweaks.presentation.model.ProxyScopeFormState -import zed.rainxch.tweaks.presentation.model.ProxyType - -fun LazyListScope.networkSection( - state: TweaksState, - onAction: (TweaksAction) -> Unit, -) { - item { - SectionHeader(text = stringResource(Res.string.section_network)) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(Res.string.proxy_scope_intro), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - ) - Spacer(Modifier.height(4.dp)) - } - - item { - OutlinedCard( - onClick = { onAction(TweaksAction.OnMirrorPickerClick) }, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(32.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = Icons.Default.NetworkCheck, - contentDescription = null, - ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(Res.string.mirror_tweaks_entry_label), - style = MaterialTheme.typography.titleMedium, - ) - } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - ) - } - } - } - item { - Spacer(Modifier.height(12.dp)) - OutlinedCard( - onClick = { onAction(TweaksAction.OnOpenCustomForgesDialog) }, - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(32.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = androidx.compose.material.icons.Icons.Default.Dns, - contentDescription = null, - ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(Res.string.custom_forges_entry_label), - style = MaterialTheme.typography.titleMedium, - ) - val count = state.customForgeHosts.size - if (count > 0) { - Text( - text = stringResource(Res.string.custom_forges_count_label, count), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else { - Text( - text = stringResource(Res.string.custom_forges_entry_subtitle), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - Icon( - imageVector = androidx.compose.material.icons.Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - ) - } - } - } - item { - Spacer(Modifier.height(16.dp)) - } - - ProxyScope.entries.forEach { scope -> - item { - ProxyScopeCard( - scope = scope, - form = state.formFor(scope), - onAction = onAction, - ) - Spacer(Modifier.height(12.dp)) - } - } -} - -private fun scopeTitleRes(scope: ProxyScope): StringResource = - when (scope) { - ProxyScope.DISCOVERY -> Res.string.proxy_scope_discovery_title - ProxyScope.DOWNLOAD -> Res.string.proxy_scope_download_title - ProxyScope.TRANSLATION -> Res.string.proxy_scope_translation_title - } - -private fun scopeDescriptionRes(scope: ProxyScope): StringResource = - when (scope) { - ProxyScope.DISCOVERY -> Res.string.proxy_scope_discovery_description - ProxyScope.DOWNLOAD -> Res.string.proxy_scope_download_description - ProxyScope.TRANSLATION -> Res.string.proxy_scope_translation_description - } - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun ProxyScopeCard( - scope: ProxyScope, - form: ProxyScopeFormState, - onAction: (TweaksAction) -> Unit, -) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = RoundedCornerShape(32.dp), - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(scopeTitleRes(scope)), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - Text( - text = stringResource(scopeDescriptionRes(scope)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp), - ) - - Spacer(Modifier.height(12.dp)) - - LazyRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(ProxyType.entries) { type -> - FilterChip( - selected = form.type == type, - onClick = { onAction(TweaksAction.OnProxyTypeSelected(scope, type)) }, - label = { - Text( - text = - when (type) { - ProxyType.NONE -> stringResource(Res.string.proxy_none) - ProxyType.SYSTEM -> stringResource(Res.string.proxy_system) - ProxyType.HTTP -> stringResource(Res.string.proxy_http) - ProxyType.SOCKS -> stringResource(Res.string.proxy_socks) - }, - fontWeight = if (form.type == type) FontWeight.Bold else FontWeight.Normal, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - ) - } - } - - AnimatedVisibility( - visible = form.type == ProxyType.NONE || form.type == ProxyType.SYSTEM, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - Column { - Spacer(Modifier.height(12.dp)) - Text( - text = - when (form.type) { - ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description) - else -> stringResource(Res.string.proxy_none_description) - }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(Modifier.height(8.dp)) - ProxyTestButton( - isInProgress = form.isTestInProgress, - enabled = !form.isTestInProgress, - onClick = { onAction(TweaksAction.OnProxyTest(scope)) }, - ) - } - } - - AnimatedVisibility( - visible = form.type == ProxyType.HTTP || form.type == ProxyType.SOCKS, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - ProxyDetailsFields( - scope = scope, - form = form, - onAction = onAction, - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun ProxyDetailsFields( - scope: ProxyScope, - form: ProxyScopeFormState, - onAction: (TweaksAction) -> Unit, -) { - val focusManager = LocalFocusManager.current - val keyboardController = LocalSoftwareKeyboardController.current - val portValue = form.port - val isPortInvalid = - portValue.isNotEmpty() && - (portValue.toIntOrNull()?.let { it !in 1..65535 } ?: true) - val hostValue = form.host - - val isHostInvalid = hostValue.isNotEmpty() && !isLikelyValidProxyHost(hostValue) - val isFormValid = - hostValue.isNotEmpty() && - !isHostInvalid && - portValue.isNotEmpty() && - portValue.toIntOrNull()?.let { it in 1..65535 } == true - - Column( - modifier = Modifier.padding(top = 12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - OutlinedTextField( - value = form.host, - onValueChange = { onAction(TweaksAction.OnProxyHostChanged(scope, it)) }, - label = { Text(stringResource(Res.string.proxy_host)) }, - placeholder = { Text("127.0.0.1") }, - singleLine = true, - isError = isHostInvalid, - supportingText = - if (isHostInvalid) { - { Text(stringResource(Res.string.proxy_host_invalid)) } - } else { - null - }, - modifier = Modifier.weight(2f), - shape = RoundedCornerShape(12.dp), - ) - - OutlinedTextField( - value = form.port, - onValueChange = { onAction(TweaksAction.OnProxyPortChanged(scope, it)) }, - label = { Text(stringResource(Res.string.proxy_port)) }, - placeholder = { Text("1080") }, - singleLine = true, - isError = isPortInvalid, - supportingText = - if (isPortInvalid) { - { Text(stringResource(Res.string.proxy_port_error)) } - } else { - null - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(12.dp), - ) - } - - OutlinedTextField( - value = form.username, - onValueChange = { onAction(TweaksAction.OnProxyUsernameChanged(scope, it)) }, - label = { Text(stringResource(Res.string.proxy_username)) }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - ) - - OutlinedTextField( - value = form.password, - onValueChange = { onAction(TweaksAction.OnProxyPasswordChanged(scope, it)) }, - label = { Text(stringResource(Res.string.proxy_password)) }, - singleLine = true, - visualTransformation = - if (form.isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - trailingIcon = { - IconButton( - onClick = { onAction(TweaksAction.OnProxyPasswordVisibilityToggle(scope)) }, - ) { - Icon( - imageVector = - if (form.isPasswordVisible) { - Icons.Default.VisibilityOff - } else { - Icons.Default.Visibility - }, - contentDescription = - if (form.isPasswordVisible) { - stringResource(Res.string.proxy_hide_password) - } else { - stringResource(Res.string.proxy_show_password) - }, - modifier = Modifier.size(20.dp), - ) - } - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - ) - - Row( - modifier = Modifier.align(Alignment.End), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - ProxyTestButton( - isInProgress = form.isTestInProgress, - - enabled = !form.isTestInProgress, - onClick = { - keyboardController?.hide() - focusManager.clearFocus() - onAction(TweaksAction.OnProxyTest(scope)) - }, - ) - - FilledTonalButton( - onClick = { - - keyboardController?.hide() - focusManager.clearFocus() - onAction(TweaksAction.OnProxySave(scope)) - }, - enabled = !form.isTestInProgress, - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.proxy_save)) - } - } - } -} - -@Composable -private fun ProxyTestButton( - isInProgress: Boolean, - enabled: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - OutlinedButton( - onClick = onClick, - enabled = enabled, - modifier = modifier, - ) { - if (isInProgress) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.proxy_test_in_progress)) - } else { - Icon( - imageVector = Icons.Default.NetworkCheck, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.proxy_test)) - } - } -} - -private fun isLikelyValidProxyHost(raw: String): Boolean { - val host = raw.trim() - if (host.isBlank()) return false - if (host.length > 253) return false - if (host.any { it.isWhitespace() }) return false - if (host.contains("://") || host.contains("/") || - host.contains("?") || host.contains("#") - ) { - return false - } - if (IPV4_PATTERN.matches(host)) return true - val ipv6Candidate = host.trim('[', ']') - if (ipv6Candidate.contains(":") && IPV6_PATTERN.matches(ipv6Candidate)) return true - return HOSTNAME_PATTERN.matches(host) -} - -private val IPV4_PATTERN = - Regex( - "^(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)" + - "(\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)){3}$", - ) - -private val IPV6_PATTERN = Regex("^[0-9A-Fa-f:]+$") - -private val HOSTNAME_PATTERN = - Regex( - "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)" + - "(?:\\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$", - ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt deleted file mode 100644 index 81e50874f..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Others.kt +++ /dev/null @@ -1,180 +0,0 @@ -package zed.rainxch.tweaks.presentation.components.sections - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.material.icons.outlined.Inventory2 -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.presentation.components.ExpressiveCard -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.tweaks.presentation.TweaksAction -import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader -import zed.rainxch.tweaks.presentation.components.ToggleSettingCard - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun LazyListScope.othersSection( - state: TweaksState, - onAction: (TweaksAction) -> Unit, -) { - item { - SectionHeader( - text = stringResource(Res.string.storage).uppercase(), - ) - - Spacer(Modifier.height(8.dp)) - - ExpressiveCard { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = Icons.Outlined.Inventory2, - contentDescription = null, - modifier = - Modifier - .size(44.dp) - .clip(RoundedCornerShape(36.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerLow) - .padding(8.dp), - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp), - horizontalAlignment = Alignment.Start, - ) { - Text( - text = stringResource(Res.string.downloaded_packages), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - - Text( - text = stringResource(Res.string.downloaded_packages_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Text( - text = "${stringResource(Res.string.current_size)} ${state.cacheSize}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = FontWeight.SemiBold, - ) - } - - FilledTonalButton( - onClick = { - onAction(TweaksAction.OnClearCacheClick) - }, - shape = RoundedCornerShape(12.dp), - colors = - ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Icon( - imageVector = Icons.Outlined.DeleteOutline, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - } - } - - Spacer(Modifier.height(16.dp)) - - ToggleSettingCard( - title = stringResource(Res.string.auto_detect_clipboard_links), - description = stringResource(Res.string.auto_detect_clipboard_description), - checked = state.autoDetectClipboardLinks, - onCheckedChange = { enabled -> - onAction(TweaksAction.OnAutoDetectClipboardToggled(enabled)) - }, - ) - - Spacer(Modifier.height(16.dp)) - - ToggleSettingCard( - title = stringResource(Res.string.hide_seen_title), - description = stringResource(Res.string.hide_seen_description), - checked = state.isHideSeenEnabled, - onCheckedChange = { enabled -> - onAction(TweaksAction.OnHideSeenToggled(enabled)) - }, - ) - - Spacer(Modifier.height(8.dp)) - - ClearSeenHistoryCard( - onClick = { - onAction(TweaksAction.OnClearSeenRepos) - }, - ) - - } -} - -@Composable -private fun ClearSeenHistoryCard(onClick: () -> Unit) { - ExpressiveCard { - Row( - modifier = - Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - ) { - Text( - text = stringResource(Res.string.clear_seen_history), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - - Spacer(Modifier.height(4.dp)) - - Text( - text = stringResource(Res.string.clear_seen_history_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } -} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/SettingsSection.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/SettingsSection.kt deleted file mode 100644 index 5eef2d946..000000000 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/SettingsSection.kt +++ /dev/null @@ -1,60 +0,0 @@ -package zed.rainxch.tweaks.presentation.components.sections - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import zed.rainxch.tweaks.presentation.TweaksAction -import zed.rainxch.tweaks.presentation.TweaksState - -fun LazyListScope.settings( - state: TweaksState, - onAction: (TweaksAction) -> Unit, -) { - appearanceSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(32.dp)) - } - - languageSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(32.dp)) - } - - networkSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(32.dp)) - } - - translationSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(12.dp)) - } - - installationSection( - state = state, - onAction = onAction, - ) - - updatesSection( - state = state, - onAction = onAction, - ) -} From 960b9926bf07c3b224757013f6ba80dca5964013 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:20:30 +0500 Subject: [PATCH 095/172] chore(tweaks): rename labels and drop ALLCAPS section headers --- .../composeResources/values/strings.xml | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 7c4d54c24..1fe5f8294 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -119,21 +119,21 @@ Profile - APPEARANCE - LANGUAGE + Appearance + Language Override the app's UI language. App language Changes menus, buttons, and messages throughout the app. Does not change content coming from GitHub. Follow system Restart to apply the new language. Restart - NETWORK - ABOUT + Network + About - Theme Color - AMOLED Black Theme - Pure black background for dark mode + Palette + True black (AMOLED) + Pure-black background — saves power on OLED screens. Selected color: %1$s @@ -145,10 +145,10 @@ Proxy Type - None + No proxy System - HTTP - SOCKS + HTTP/HTTPS + SOCKS5 Host Port Username (optional) @@ -157,7 +157,7 @@ Proxy settings saved Uses your device's proxy settings Port must be 1–65535 - Direct connection, no proxy + The app connects to the internet without a proxy. Failed to save proxy settings Proxy host is required Enter a valid hostname or IP address @@ -174,7 +174,7 @@ Unexpected response: HTTP %1$d Connection test failed Each category can use its own proxy. Configure them independently. - Discovery (GitHub API) + Search & metadata Home, search, repo details, and update checks Downloads APK downloads and auto-updates @@ -611,16 +611,16 @@ Open GitHub Link GitHub link detected in clipboard - Auto-detect clipboard links - Automatically detect GitHub links from clipboard when opening search + Detect repo links in clipboard + When you copy a github.com or codeberg.org link, we\'ll prompt to open it. Detected Links Open in app No GitHub link found in clipboard Storage - Downloaded Packages - APKs and installers from GitHub releases - Current size: + Downloaded APKs + We keep installers around so updates resume fast. + Using: Delete All Delete all downloads? This will permanently remove all downloaded APKs and installers (%1$s). You can re-download them anytime. @@ -644,8 +644,8 @@ Installation - Default - Standard system install dialog + System installer + Asks each time. Works on every device. Shizuku Silent install without prompts Shizuku is not installed @@ -672,8 +672,8 @@ No root Allow root access Root isn't available on this device. Install Magisk, KernelSU, or APatch first. - Auto-update apps - Automatically download and install updates in background via the selected silent installer + Install updates in the background + Apply downloaded updates without notifying you each time. Installer attribution Apps can see which installer placed them on your device. By default this is the system shell. @@ -688,14 +688,14 @@ Some apps detect when their installer changes and may refuse to run, or fail security checks (e.g. Play Integrity, banking apps). Updates - Update check interval - How often to check for app updates in background - Background update check - Periodically check for updates in the background. Turn off to save battery — you can still check manually from any app's details screen. - 3h - 6h - 12h - 24h + Check every + How often to look for new releases. + Check automatically + Look for new releases on a schedule. Turn off to save battery — you can still check manually from any app\'s details screen. + Every 3 hours + Every 6 hours + Every 12 hours + Daily Allow background updates Your device aggressively kills background tasks. Whitelist GitHub Store from battery optimization so scheduled update checks and silent installs can run reliably. @@ -788,10 +788,10 @@ Wide Extra wide - Hide Seen Repositories - Hide repositories you have already viewed from discovery feeds - Clear Seen History - Reset all seen repositories so they appear again in feeds + Hide repos I\'ve already viewed + Skip seen repos in feeds and search. + Clear viewed history + Forget which repos you\'ve already opened. Seen history cleared Viewed You own this repo @@ -1088,7 +1088,7 @@ Done - Download Mirror + GitHub mirror Download Mirror Used for downloading release assets. GitHub API calls always go direct. Most users should leave this on Direct GitHub. From 0d8653d33d631f0847f4adbedf912c19822f09ed Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:24:46 +0500 Subject: [PATCH 096/172] feat(deeplink): tweaks app-info and licenses routes --- .../kotlin/zed/rainxch/githubstore/Main.kt | 18 ++++++++++++++++++ .../githubstore/app/deeplink/DeepLinkParser.kt | 18 ++++++++++++++++++ .../composeResources/values/strings.xml | 5 +++++ 3 files changed, 41 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt index f860905c3..e9d289af5 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt @@ -102,6 +102,24 @@ fun App(deepLinkUri: String? = null) { } } + DeepLinkDestination.Tweaks -> { + navController.navigate(GithubStoreGraph.TweaksScreen) { + launchSingleTop = true + } + } + + DeepLinkDestination.TweaksAppInfo -> { + navController.navigate(GithubStoreGraph.TweaksAppInfoScreen) { + launchSingleTop = true + } + } + + DeepLinkDestination.TweaksLicenses -> { + navController.navigate(GithubStoreGraph.LicensesScreen) { + launchSingleTop = true + } + } + DeepLinkDestination.None -> { } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt index 57f3ef939..50d110313 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt @@ -18,6 +18,12 @@ sealed interface DeepLinkDestination { val state: String, ) : DeepLinkDestination + data object Tweaks : DeepLinkDestination + + data object TweaksAppInfo : DeepLinkDestination + + data object TweaksLicenses : DeepLinkDestination + data object None : DeepLinkDestination } @@ -99,6 +105,18 @@ object DeepLinkParser { DeepLinkDestination.None } + uri == "githubstore://tweaks" || uri == "githubstore://tweaks/" -> { + DeepLinkDestination.Tweaks + } + + uri == "githubstore://tweaks/app-info" -> { + DeepLinkDestination.TweaksAppInfo + } + + uri == "githubstore://tweaks/licenses" -> { + DeepLinkDestination.TweaksLicenses + } + uri.startsWith("https://github-store.org/app/") -> { extractQueryParam(uri, "repo")?.let { encodedRepoParam -> val decoded = urlDecode(encodedRepoParam) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 1fe5f8294..9db042b96 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1230,6 +1230,11 @@ Coming in a follow-up This screen is part of the in-progress redesign. The settings still work — they\'re just being moved here from the old layout. Back + Help + About GitHub Store + Send feedback… + Open source licenses + Privacy policy APK Inspect From 21fa4bc4759051250db3385b0bc8f630073dbfcd Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:24:47 +0500 Subject: [PATCH 097/172] feat(desktop): Help menu and macOS about handler --- .../zed/rainxch/githubstore/DesktopApp.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt index d1adaa868..94147d19f 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isMetaPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type +import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.flow.first @@ -26,9 +27,17 @@ import zed.rainxch.githubstore.app.di.initKoin import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.app_icon import zed.rainxch.githubstore.core.presentation.res.app_name +import zed.rainxch.githubstore.core.presentation.res.menubar_help_about +import zed.rainxch.githubstore.core.presentation.res.menubar_help_feedback +import zed.rainxch.githubstore.core.presentation.res.menubar_help_licenses +import zed.rainxch.githubstore.core.presentation.res.menubar_help_menu +import zed.rainxch.githubstore.core.presentation.res.menubar_help_privacy import java.awt.Desktop +import java.net.URI import kotlin.system.exitProcess +private const val PRIVACY_POLICY_URL = "https://github-store.org/privacy" + private const val LANGUAGE_PREF_READ_TIMEOUT_MS = 2000L fun main(args: Array) { @@ -82,6 +91,13 @@ fun main(args: Array) { deepLinkUri = event.uri.toString() } } + if (desktop.isSupported(Desktop.Action.APP_ABOUT)) { + runCatching { + desktop.setAboutHandler { + deepLinkUri = "githubstore://tweaks/app-info" + } + } + } } } @@ -102,6 +118,34 @@ fun main(args: Array) { } }, ) { + MenuBar { + Menu(text = stringResource(Res.string.menubar_help_menu)) { + Item( + text = stringResource(Res.string.menubar_help_about), + onClick = { deepLinkUri = "githubstore://tweaks/app-info" }, + ) + Item( + text = stringResource(Res.string.menubar_help_feedback), + onClick = { deepLinkUri = "githubstore://tweaks" }, + ) + Item( + text = stringResource(Res.string.menubar_help_licenses), + onClick = { deepLinkUri = "githubstore://tweaks/licenses" }, + ) + Item( + text = stringResource(Res.string.menubar_help_privacy), + onClick = { + runCatching { + if (Desktop.isDesktopSupported() && + Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) + ) { + Desktop.getDesktop().browse(URI(PRIVACY_POLICY_URL)) + } + } + }, + ) + } + } App(deepLinkUri = deepLinkUri) } } From 661c25e88bf48d47e3a40d538a2d372a7051ac1f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:27:19 +0500 Subject: [PATCH 098/172] fix(tweaks): drop duplicate section headers inside sub-screens --- .../components/sections/Appearance.kt | 3 -- .../components/sections/Installation.kt | 16 ---------- .../components/sections/Translation.kt | 2 -- .../language/TweaksLanguageRoot.kt | 30 +++++-------------- 4 files changed, 7 insertions(+), 44 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index 7d1b5b253..835fdedba 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -69,9 +69,6 @@ fun LazyListScope.appearanceSection( onAction: (TweaksAction) -> Unit, ) { item { - SectionHeader(text = stringResource(Res.string.section_appearance)) - Spacer(Modifier.height(8.dp)) - ThemePickerCard( isDarkTheme = state.isDarkTheme, selectedPalette = state.selectedThemeColor, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index 405fc614f..c92c0c227 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -68,14 +68,6 @@ fun LazyListScope.installationSection( if (getPlatform() != Platform.ANDROID) return item { - Spacer(Modifier.height(32.dp)) - - SectionHeader( - text = stringResource(Res.string.section_installation).uppercase() - ) - - Spacer(Modifier.height(8.dp)) - InstallerTypeCard( selectedType = state.installerType, shizukuAvailability = state.shizukuAvailability, @@ -291,14 +283,6 @@ fun LazyListScope.updatesSection( if (getPlatform() != Platform.ANDROID) return item { - Spacer(Modifier.height(32.dp)) - - SectionHeader( - text = stringResource(Res.string.section_updates).uppercase() - ) - - Spacer(Modifier.height(8.dp)) - if (state.showBatteryOptimizationCard) { BatteryOptimizationCard( onOpenSettings = { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt index 75d68ec94..36facc4f1 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt @@ -66,8 +66,6 @@ fun LazyListScope.translationSection( onAction: (TweaksAction) -> Unit, ) { item { - SectionHeader(text = stringResource(Res.string.section_translation)) - Spacer(Modifier.height(4.dp)) Text( text = stringResource(Res.string.translation_intro), style = MaterialTheme.typography.bodySmall, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt index c8506d742..9cbfc5446 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt @@ -77,29 +77,13 @@ fun TweaksLanguageRoot( showRestartBanner = state.restartBannerVisible, ) { item(key = "language_intro") { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(Res.string.language_picker_title), - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.height(4.dp)) - Text( - text = "The app restarts when you switch language.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - Spacer(Modifier.height(12.dp)) + Text( + text = "The app restarts when you switch language.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp), + ) + Spacer(Modifier.height(8.dp)) } item(key = "language_search") { From 462c9724a5f498e3d6730d6078a77e644e083c9d Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:48:19 +0500 Subject: [PATCH 099/172] feat(tweaks): per-scope proxy mini-editor inside override row --- .../connection/TweaksConnectionRoot.kt | 118 +++++++++++++++--- 1 file changed, 103 insertions(+), 15 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index 0621a1e05..ee344df7e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -451,6 +451,7 @@ private fun OverridesCard( onToggle = { useMain -> onAction(TweaksAction.OnScopeUseMainToggled(scope, useMain)) }, + onAction = onAction, ) } } @@ -463,6 +464,7 @@ private fun ScopeOverrideRow( useMain: Boolean, scopeForm: ProxyScopeFormState, onToggle: (Boolean) -> Unit, + onAction: (TweaksAction) -> Unit, ) { val (title, subtitle) = when (scope) { ProxyScope.DISCOVERY -> "Search & metadata" to "GitHub API, search, repo details." @@ -503,28 +505,114 @@ private fun ScopeOverrideRow( AnimatedVisibility(visible = !useMain) { Column { - Spacer(Modifier.height(8.dp)) - Text( - text = scopeStatusLabel(scopeForm), - style = MaterialTheme.typography.labelSmall.copy( - fontFamily = FontFamily.Monospace, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, + Spacer(Modifier.height(12.dp)) + ModePillSegment( + selected = scopeForm.type, + onSelected = { onAction(TweaksAction.OnProxyTypeSelected(scope, it)) }, ) + + AnimatedVisibility( + visible = scopeForm.type == ProxyType.HTTP || + scopeForm.type == ProxyType.SOCKS, + ) { + Column { + Spacer(Modifier.height(12.dp)) + ProxyFormFields( + form = scopeForm, + onHostChange = { + onAction(TweaksAction.OnProxyHostChanged(scope, it)) + }, + onPortChange = { + onAction(TweaksAction.OnProxyPortChanged(scope, it)) + }, + onUserChange = { + onAction(TweaksAction.OnProxyUsernameChanged(scope, it)) + }, + onPassChange = { + onAction(TweaksAction.OnProxyPasswordChanged(scope, it)) + }, + onPassVisibility = { + onAction(TweaksAction.OnProxyPasswordVisibilityToggle(scope)) + }, + ) + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedButton( + onClick = { onAction(TweaksAction.OnProxyTest(scope)) }, + shape = Radii.chip, + modifier = Modifier.weight(1f), + enabled = !scopeForm.isTestInProgress, + ) { + if (scopeForm.isTestInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + } else { + Icon( + imageVector = Icons.Default.NetworkCheck, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.size(6.dp)) + Text(text = "Test ${title.lowercase()}") + } + FilledTonalButton( + onClick = { onAction(TweaksAction.OnProxySave(scope)) }, + shape = WonkySquircleShape.CtaPrimary, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(6.dp)) + Text(text = stringResource(Res.string.proxy_save)) + } + } + } + } + + AnimatedVisibility( + visible = scopeForm.type == ProxyType.NONE || + scopeForm.type == ProxyType.SYSTEM, + ) { + Column { + Spacer(Modifier.height(12.dp)) + FilledTonalButton( + onClick = { onAction(TweaksAction.OnProxySave(scope)) }, + shape = WonkySquircleShape.CtaPrimary, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.size(6.dp)) + Text(text = stringResource(Res.string.proxy_save)) + } + } + } } } } } } -private fun scopeStatusLabel(form: ProxyScopeFormState): String = - when (form.type) { - ProxyType.NONE -> "Custom: no proxy" - ProxyType.SYSTEM -> "Custom: system" - ProxyType.HTTP -> "Custom: HTTP ${form.host.ifBlank { "—" }}:${form.port.ifBlank { "—" }}" - ProxyType.SOCKS -> "Custom: SOCKS5 ${form.host.ifBlank { "—" }}:${form.port.ifBlank { "—" }}" - } - @Composable private fun UseMainSegment( useMain: Boolean, From b54c09193d1cc14bcfeb6cab2e3adfcaedfa38c7 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:55:31 +0500 Subject: [PATCH 100/172] fix(tweaks): mode pills wrap with FlowRow, trim button labels --- .../composeResources/values/strings.xml | 2 +- .../connection/TweaksConnectionRoot.kt | 38 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 9db042b96..94de924af 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -153,7 +153,7 @@ Port Username (optional) Password (optional) - Save Proxy + Save Proxy settings saved Uses your device's proxy settings Port must be 1–65535 diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index ee344df7e..6a68f2e8d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -225,7 +227,7 @@ private fun MainConnectionCard( ) } Spacer(Modifier.size(6.dp)) - Text(text = "Test main connection") + Text(text = "Test") } FilledTonalButton( onClick = { onAction(TweaksAction.OnMasterProxySave) }, @@ -351,6 +353,7 @@ private fun ProxyFormFields( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ModePillSegment( selected: ProxyType, @@ -363,21 +366,18 @@ private fun ModePillSegment( ProxyType.SOCKS to ("SOCKS5" to "Tor, SSH tunnels."), ) - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { items.forEach { (type, labels) -> val isSelected = type == selected val container = if (isSelected) { MaterialTheme.colorScheme.primary } else { - Color.Transparent + MaterialTheme.colorScheme.surfaceContainerLow } val content = if (isSelected) { MaterialTheme.colorScheme.onPrimary @@ -386,11 +386,10 @@ private fun ModePillSegment( } Box( modifier = Modifier - .weight(1f) - .height(40.dp) - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(50)) .background(container) - .clickable { onSelected(type) }, + .clickable { onSelected(type) } + .padding(horizontal = 14.dp, vertical = 10.dp), contentAlignment = Alignment.Center, ) { Text( @@ -399,6 +398,7 @@ private fun ModePillSegment( fontWeight = FontWeight.SemiBold, ), color = content, + maxLines = 1, ) } } @@ -409,7 +409,7 @@ private fun ModePillSegment( text = caption, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 4.dp), ) } } @@ -559,15 +559,15 @@ private fun ScopeOverrideRow( ) } Spacer(Modifier.size(6.dp)) - Text(text = "Test ${title.lowercase()}") + Text(text = "Test") } FilledTonalButton( onClick = { onAction(TweaksAction.OnProxySave(scope)) }, - shape = WonkySquircleShape.CtaPrimary, + shape = RoundedCornerShape(50), modifier = Modifier.weight(1f), colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, ), ) { Icon( From a9f1a81ef3f6190873c1d901cc2e189d0a9c8d4e Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 19:55:33 +0500 Subject: [PATCH 101/172] refactor(tweaks): move Hidden repositories from Updates to Privacy --- .../app/navigation/AppNavigation.kt | 10 ++-- .../components/sections/Installation.kt | 6 -- .../presentation/privacy/TweaksPrivacyRoot.kt | 59 +++++++++++++++++++ .../presentation/updates/TweaksUpdatesRoot.kt | 2 - 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 391a692dc..f74719adb 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -774,11 +774,6 @@ fun AppNavigation( launchSingleTop = true } }, - onNavigateToHiddenRepositories = { - navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { - launchSingleTop = true - } - }, ) } @@ -791,6 +786,11 @@ fun AppNavigation( composable { TweaksPrivacyRoot( onNavigateBack = { navController.popBackStack() }, + onNavigateToHiddenRepositories = { + navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + launchSingleTop = true + } + }, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index c92c0c227..2d8a52be5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -326,12 +326,6 @@ fun LazyListScope.updatesSection( SkippedUpdatesEntryCard( onClick = { onAction(TweaksAction.OnSkippedUpdatesClick) }, ) - - Spacer(Modifier.height(12.dp)) - - HiddenRepositoriesEntryCard( - onClick = { onAction(TweaksAction.OnHiddenRepositoriesClick) }, - ) } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt index 58bd24fea..7d4c5928d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -42,6 +43,7 @@ import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold @Composable fun TweaksPrivacyRoot( onNavigateBack: () -> Unit, + onNavigateToHiddenRepositories: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -105,6 +107,15 @@ fun TweaksPrivacyRoot( subtitle = "Forget which repos you've already opened.", onClick = { viewModel.onAction(TweaksAction.OnClearSeenHistoryRequest) }, ) + Spacer(Modifier.height(8.dp)) + } + + item(key = "hidden_repos_row") { + DrillRow( + title = "Hidden repositories", + subtitle = "Repos you've muted from feeds and search.", + onClick = onNavigateToHiddenRepositories, + ) } } @@ -269,6 +280,54 @@ private fun ToggleCard( } } +@Composable +private fun DrillRow( + title: String, + subtitle: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + @Composable private fun DestructiveRow( title: String, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt index df5eea2e6..dde86be2a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/updates/TweaksUpdatesRoot.kt @@ -18,7 +18,6 @@ import zed.rainxch.tweaks.presentation.components.sections.updatesSection fun TweaksUpdatesRoot( onNavigateBack: () -> Unit, onNavigateToSkippedUpdates: () -> Unit, - onNavigateToHiddenRepositories: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -38,7 +37,6 @@ fun TweaksUpdatesRoot( onAction = { action -> when (action) { TweaksAction.OnSkippedUpdatesClick -> onNavigateToSkippedUpdates() - TweaksAction.OnHiddenRepositoriesClick -> onNavigateToHiddenRepositories() else -> viewModel.onAction(action) } }, From af7cfa74a6eb1d8a22534ab09f511adb8d9cd6c6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 20:51:50 +0500 Subject: [PATCH 102/172] feat(tweaks): expressive per-row accent colors on entry tiles --- .../rainxch/tweaks/presentation/TweaksRoot.kt | 16 +++++++++ .../presentation/appinfo/TweaksAppInfoRoot.kt | 21 +++++++++-- .../presentation/components/TweaksAccents.kt | 18 ++++++++++ .../presentation/components/TweaksEntryRow.kt | 18 ++++++++-- .../presentation/privacy/TweaksPrivacyRoot.kt | 35 +++++++++++++++++++ .../presentation/sources/TweaksSourcesRoot.kt | 19 ++++++++-- 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 781313afb..653041c83 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -63,6 +64,7 @@ import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.components.RestartBanner import zed.rainxch.tweaks.presentation.components.SectionHeader +import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.tweaks.presentation.components.TweaksEntryRow import zed.rainxch.tweaks.presentation.components.TweaksSearchField import zed.rainxch.tweaks.presentation.feedback.components.FeedbackBottomSheet @@ -171,6 +173,7 @@ private data class TweaksHubEntry( val subtitle: String, val icon: ImageVector, val onClick: () -> Unit, + val accent: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Unspecified, ) private data class TweaksHubBlock( @@ -213,12 +216,14 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.Palette, onClick = onNavigateToAppearance, + accent = TweaksAccents.Lavender, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_language), subtitle = tapToManage, icon = Icons.Outlined.Translate, onClick = onNavigateToLanguage, + accent = TweaksAccents.Mint, ), ), ), @@ -230,18 +235,21 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.Wifi, onClick = onNavigateToConnection, + accent = TweaksAccents.Sky, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_sources), subtitle = tapToManage, icon = Icons.Outlined.Hub, onClick = onNavigateToSources, + accent = TweaksAccents.Blush, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_translation), subtitle = tapToManage, icon = Icons.Outlined.GTranslate, onClick = onNavigateToTranslation, + accent = TweaksAccents.Peach, ), ), ), @@ -253,12 +261,14 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.InstallMobile, onClick = onNavigateToInstallMethod, + accent = TweaksAccents.Sage, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_updates), subtitle = tapToManage, icon = Icons.Outlined.Update, onClick = onNavigateToUpdates, + accent = TweaksAccents.Amber, ), ), ), @@ -270,18 +280,21 @@ fun TweaksHubScreen( subtitle = state.cacheSize.ifBlank { tapToManage }, icon = Icons.Outlined.Inventory2, onClick = onNavigateToStorage, + accent = TweaksAccents.Periwinkle, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_privacy), subtitle = tapToManage, icon = Icons.Outlined.PrivacyTip, onClick = onNavigateToPrivacy, + accent = TweaksAccents.Rose, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_access_tokens), subtitle = tapToManage, icon = Icons.Outlined.VpnKey, onClick = onNavigateToHostTokens, + accent = TweaksAccents.Gold, ), ), ), @@ -293,12 +306,14 @@ fun TweaksHubScreen( subtitle = state.versionName.ifBlank { "—" }, icon = Icons.Outlined.Info, onClick = onNavigateToAppInfo, + accent = TweaksAccents.Aqua, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_feedback), subtitle = stringResource(Res.string.feedback_hub_subtitle), icon = Icons.Outlined.Feedback, onClick = onSendFeedbackClick, + accent = TweaksAccents.Tan, ), ), ), @@ -400,6 +415,7 @@ fun TweaksHubScreen( subtitle = entry.subtitle, icon = entry.icon, onClick = entry.onClick, + accentColor = entry.accent, ) Spacer(Modifier.height(8.dp)) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index c64978da8..8e9e27425 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontFamily @@ -40,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_app_info import zed.rainxch.tweaks.presentation.TweaksAction @@ -79,6 +81,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.NewReleases, title = "What's new", subtitle = "Past release notes.", + accent = TweaksAccents.Peach, onClick = onNavigateToWhatsNewHistory, ) Spacer(Modifier.height(8.dp)) @@ -89,6 +92,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.Code, title = "Open source licenses", subtitle = "Libraries used in the app.", + accent = TweaksAccents.Sage, onClick = onNavigateToLicenses, ) Spacer(Modifier.height(8.dp)) @@ -99,6 +103,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.Description, title = "Privacy policy", subtitle = "View on github-store.org.", + accent = TweaksAccents.Rose, onClick = { runCatching { uriHandler.openUri(PRIVACY_POLICY_URL) } }, @@ -111,6 +116,7 @@ fun TweaksAppInfoRoot( icon = Icons.AutoMirrored.Outlined.OpenInNew, title = "Source code on GitHub", subtitle = "View this app's source.", + accent = TweaksAccents.Aqua, onClick = { runCatching { uriHandler.openUri(SOURCE_CODE_URL) } }, @@ -184,7 +190,18 @@ private fun ActionRow( title: String, subtitle: String, onClick: () -> Unit, + accent: Color = Color.Unspecified, ) { + val tileBg = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.surfaceContainerHigh + } else { + accent.copy(alpha = 0.14f) + } + val tint = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + accent + } Surface( modifier = Modifier .fillMaxWidth() @@ -205,13 +222,13 @@ private fun ActionRow( modifier = Modifier .size(40.dp) .clip(Radii.chip) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), + .background(tileBg), contentAlignment = Alignment.Center, ) { Icon( imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = tint, modifier = Modifier.size(22.dp), ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt new file mode 100644 index 000000000..60cc404e1 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt @@ -0,0 +1,18 @@ +package zed.rainxch.tweaks.presentation.components + +import androidx.compose.ui.graphics.Color + +object TweaksAccents { + val Lavender = Color(0xFFB19CD9) + val Mint = Color(0xFF88C9BF) + val Sky = Color(0xFF8FB8E0) + val Blush = Color(0xFFE89FA5) + val Peach = Color(0xFFE8B179) + val Sage = Color(0xFFA1C99B) + val Amber = Color(0xFFE2B358) + val Periwinkle = Color(0xFFA9A4E0) + val Rose = Color(0xFFD98AAA) + val Gold = Color(0xFFD9B856) + val Aqua = Color(0xFF7FBCC9) + val Tan = Color(0xFFC9A07C) +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt index f1e26ce94..fe0c6e5a6 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription @@ -38,7 +39,20 @@ fun TweaksEntryRow( modifier: Modifier = Modifier, subtitle: String? = null, badge: (@Composable () -> Unit)? = null, + accentColor: Color = Color.Unspecified, ) { + val effectiveAccent = + if (accentColor == Color.Unspecified) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + accentColor + } + val tileBackground = + if (accentColor == Color.Unspecified) { + MaterialTheme.colorScheme.surfaceContainerHigh + } else { + accentColor.copy(alpha = 0.14f) + } Surface( modifier = modifier .fillMaxWidth() @@ -73,13 +87,13 @@ fun TweaksEntryRow( modifier = Modifier .size(40.dp) .clip(Radii.chip) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), + .background(tileBackground), contentAlignment = Alignment.Center, ) { Icon( imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = effectiveAccent, modifier = Modifier.size(22.dp), ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt index 7d4c5928d..5e16bc804 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -2,6 +2,7 @@ package zed.rainxch.tweaks.presentation.privacy import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,6 +15,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState @@ -27,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -34,6 +40,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.overlays.GhsConfirmDialog import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_privacy import zed.rainxch.tweaks.presentation.TweaksAction @@ -112,8 +119,10 @@ fun TweaksPrivacyRoot( item(key = "hidden_repos_row") { DrillRow( + icon = Icons.Outlined.VisibilityOff, title = "Hidden repositories", subtitle = "Repos you've muted from feeds and search.", + accent = TweaksAccents.Periwinkle, onClick = onNavigateToHiddenRepositories, ) } @@ -282,10 +291,22 @@ private fun ToggleCard( @Composable private fun DrillRow( + icon: ImageVector, title: String, subtitle: String, onClick: () -> Unit, + accent: Color = Color.Unspecified, ) { + val tileBg = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.surfaceContainerHigh + } else { + accent.copy(alpha = 0.14f) + } + val tint = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + accent + } Surface( modifier = Modifier .fillMaxWidth() @@ -302,6 +323,20 @@ private fun DrillRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(tileBg), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(22.dp), + ) + } Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt index d606c9e98..db61fb1b2 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -38,6 +39,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_sources import zed.rainxch.tweaks.presentation.TweaksAction @@ -94,6 +96,7 @@ fun TweaksSourcesRoot( icon = Icons.Outlined.NetworkCheck, title = "GitHub mirror", subtitle = "Default (github.com)", + accent = TweaksAccents.Sky, onClick = onNavigateToMirrorPicker, ) Spacer(Modifier.height(8.dp)) @@ -109,6 +112,7 @@ fun TweaksSourcesRoot( } else { "$count host${if (count == 1) "" else "s"}" }, + accent = TweaksAccents.Mint, onClick = { viewModel.onAction(TweaksAction.OnOpenCustomForgesDialog) }, ) } @@ -152,7 +156,18 @@ private fun DrillRow( title: String, subtitle: String, onClick: () -> Unit, + accent: Color = Color.Unspecified, ) { + val tileBg = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.surfaceContainerHigh + } else { + accent.copy(alpha = 0.14f) + } + val tint = if (accent == Color.Unspecified) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + accent + } Surface( modifier = Modifier .fillMaxWidth() @@ -173,13 +188,13 @@ private fun DrillRow( modifier = Modifier .size(40.dp) .clip(Radii.chip) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), + .background(tileBg), contentAlignment = Alignment.Center, ) { Icon( imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = tint, modifier = Modifier.size(22.dp), ) } From a5c21b06fbc40b314459b71135222967f783dc34 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 21:12:03 +0500 Subject: [PATCH 103/172] feat(core): GhsTextField squircle input vocab, swap into Connection form --- .../components/inputs/GhsTextField.kt | 180 ++++++++++++++++++ .../connection/TweaksConnectionRoot.kt | 63 ++---- 2 files changed, 196 insertions(+), 47 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt new file mode 100644 index 000000000..788e8e335 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt @@ -0,0 +1,180 @@ +package zed.rainxch.core.presentation.components.inputs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.theme.tokens.Radii + +@Composable +fun GhsTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + placeholder: String? = null, + leadingIcon: ImageVector? = null, + trailingIcon: (@Composable () -> Unit)? = null, + supportingText: String? = null, + isError: Boolean = false, + enabled: Boolean = true, + singleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + var focused by remember { mutableStateOf(false) } + val borderColor = when { + isError -> MaterialTheme.colorScheme.error + focused -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.outline + } + val contentColor = if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Column(modifier = modifier) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.chip), + shape = Radii.chip, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = androidx.compose.foundation.BorderStroke( + width = if (focused) 1.5.dp else 1.dp, + color = borderColor, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + if (leadingIcon != null) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + + Column(modifier = Modifier.weight(1f)) { + if (!label.isNullOrBlank() && (focused || value.isNotEmpty())) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = if (focused) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + Box(contentAlignment = Alignment.CenterStart) { + if (value.isEmpty() && !focused) { + val showText = label ?: placeholder + if (showText != null) { + Text( + text = showText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focused = it.isFocused }, + singleLine = singleLine, + enabled = enabled, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + textStyle = LocalTextStyle.current.merge( + TextStyle(color = contentColor), + ).copy( + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + ) + } + } + + if (trailingIcon != null) { + trailingIcon() + } + } + } + + if (!supportingText.isNullOrBlank()) { + Text( + text = supportingText, + style = MaterialTheme.typography.labelSmall, + color = if (isError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.padding(start = 12.dp, top = 4.dp), + ) + } + } +} + +@Composable +fun GhsPasswordVisibilityIcon( + visible: Boolean, + onToggle: () -> Unit, +) { + IconButton(onClick = onToggle, modifier = Modifier.size(36.dp)) { + Icon( + imageVector = if (visible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } +} + +fun passwordVisualTransformation(visible: Boolean): VisualTransformation = + if (visible) VisualTransformation.None else PasswordVisualTransformation() diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index 6a68f2e8d..6e01df8c2 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -20,17 +20,12 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.NetworkCheck import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -45,8 +40,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch @@ -54,6 +47,9 @@ import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.ProxyScope +import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon +import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents @@ -285,71 +281,44 @@ private fun ProxyFormFields( onPassChange: (String) -> Unit, onPassVisibility: () -> Unit, ) { - val colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.outline, - unfocusedBorderColor = MaterialTheme.colorScheme.outline, - ) - Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedTextField( + GhsTextField( value = form.host, onValueChange = onHostChange, modifier = Modifier.weight(2f), - label = { Text(text = stringResource(Res.string.proxy_host)) }, - singleLine = true, - shape = Radii.chip, - colors = colors, + label = stringResource(Res.string.proxy_host), ) - OutlinedTextField( + GhsTextField( value = form.port, onValueChange = onPortChange, modifier = Modifier.weight(1f), - label = { Text(text = stringResource(Res.string.proxy_port)) }, - singleLine = true, - shape = Radii.chip, + label = stringResource(Res.string.proxy_port), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - colors = colors, ) } Spacer(Modifier.height(8.dp)) - OutlinedTextField( + GhsTextField( value = form.username, onValueChange = onUserChange, modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(Res.string.proxy_username)) }, - singleLine = true, - shape = Radii.chip, - colors = colors, + label = stringResource(Res.string.proxy_username), ) Spacer(Modifier.height(8.dp)) - OutlinedTextField( + GhsTextField( value = form.password, onValueChange = onPassChange, modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(Res.string.proxy_password)) }, - singleLine = true, - visualTransformation = if (form.isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + label = stringResource(Res.string.proxy_password), + visualTransformation = passwordVisualTransformation(form.isPasswordVisible), trailingIcon = { - IconButton(onClick = onPassVisibility) { - Icon( - imageVector = if (form.isPasswordVisible) { - Icons.Filled.VisibilityOff - } else { - Icons.Filled.Visibility - }, - contentDescription = null, - ) - } + GhsPasswordVisibilityIcon( + visible = form.isPasswordVisible, + onToggle = onPassVisibility, + ) }, - shape = Radii.chip, - colors = colors, ) } From d202a179c5ce0e862683515889f2ff4154d4b1f0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 21:32:05 +0500 Subject: [PATCH 104/172] refactor: swap OutlinedTextField for GhsTextField across app --- .../zed/rainxch/apps/presentation/AppsRoot.kt | 18 +- .../AdvancedAppSettingsBottomSheet.kt | 35 ++-- .../components/LinkAppBottomSheet.kt | 90 +++------- .../import/components/RepoSearchOverride.kt | 1 + .../presentation/starred/StarredPickerRoot.kt | 14 +- .../auth/presentation/AuthenticationRoot.kt | 43 ++--- .../presentation/components/LanguagePicker.kt | 11 +- .../components/FilterSortControls.kt | 22 +-- .../favourites/presentation/FavouritesRoot.kt | 16 +- .../starred/presentation/StarredReposRoot.kt | 16 +- .../components/CustomForgesDialog.kt | 6 +- .../components/sections/Installation.kt | 16 +- .../components/sections/Translation.kt | 168 ++++-------------- .../feedback/components/ConditionalFields.kt | 1 + .../components/FeedbackBottomSheet.kt | 6 +- .../presentation/hosttokens/HostTokensRoot.kt | 53 +++--- .../mirror/components/CustomMirrorDialog.kt | 6 +- 17 files changed, 157 insertions(+), 365 deletions(-) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index be1fc928c..69b2b6cd2 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -63,8 +63,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable @@ -80,7 +78,6 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -110,6 +107,7 @@ import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.apps.presentation.model.UpdateState import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme @@ -555,23 +553,15 @@ fun AppsScreen( Column( modifier = Modifier.fillMaxSize(), ) { - TextField( + GhsTextField( value = state.searchQuery, onValueChange = { onAction(AppsAction.OnSearchChange(it)) }, - leadingIcon = { - Icon(Icons.Default.Search, contentDescription = null) - }, - placeholder = { Text(stringResource(Res.string.search_your_apps)) }, + leadingIcon = Icons.Default.Search, + placeholder = stringResource(Res.string.search_your_apps), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), - shape = CircleShape, - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), ) if (state.isCheckingForUpdates) { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt index 81c1f8fea..a61859f32 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt @@ -30,9 +30,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -100,15 +100,18 @@ fun AdvancedAppSettingsBottomSheet( Spacer(Modifier.height(20.dp)) - OutlinedTextField( + val advancedSupporting = when { + state.advancedFilterError != null -> + stringResource(Res.string.asset_filter_invalid) + else -> stringResource(Res.string.asset_filter_help) + } + GhsTextField( value = state.advancedFilterDraft, onValueChange = { onAction(AppsAction.OnAdvancedFilterChanged(it)) }, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(Res.string.asset_filter_label)) }, - placeholder = { Text(stringResource(Res.string.asset_filter_placeholder)) }, - leadingIcon = { - Icon(Icons.Default.FilterAlt, contentDescription = null) - }, + label = stringResource(Res.string.asset_filter_label), + placeholder = stringResource(Res.string.asset_filter_placeholder), + leadingIcon = Icons.Default.FilterAlt, trailingIcon = { if (state.advancedFilterDraft.isNotEmpty()) { TextButton(onClick = { onAction(AppsAction.OnAdvancedClearFilter) }) { @@ -118,24 +121,8 @@ fun AdvancedAppSettingsBottomSheet( }, singleLine = true, isError = state.advancedFilterError != null, - supportingText = { - Text( - text = - when { - state.advancedFilterError != null -> - stringResource(Res.string.asset_filter_invalid) - else -> stringResource(Res.string.asset_filter_help) - }, - color = - if (state.advancedFilterError != null) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - }, + supportingText = advancedSupporting, enabled = !state.advancedSavingFilter, - shape = RoundedCornerShape(12.dp), ) Spacer(Modifier.height(12.dp)) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt index c8be8afb1..cb2c4b5a3 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt @@ -33,19 +33,16 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -166,26 +163,13 @@ private fun PickAppStep( Spacer(Modifier.height(12.dp)) - TextField( + GhsTextField( value = searchQuery, onValueChange = onSearchChange, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)), - placeholder = { - Text(stringResource(Res.string.search_apps_hint)) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null, - ) - }, + modifier = Modifier.fillMaxWidth(), + placeholder = stringResource(Res.string.search_apps_hint), + leadingIcon = Icons.Default.Search, singleLine = true, - colors = TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), ) Spacer(Modifier.height(8.dp)) @@ -677,18 +661,15 @@ private fun EnterUrlStep( Spacer(Modifier.height(16.dp)) - OutlinedTextField( + GhsTextField( value = repoUrl, onValueChange = onUrlChanged, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(Res.string.enter_repo_url)) }, - placeholder = { Text(stringResource(Res.string.repo_url_hint)) }, + label = stringResource(Res.string.enter_repo_url), + placeholder = stringResource(Res.string.repo_url_hint), singleLine = true, isError = validationError != null, - supportingText = validationError?.let { - { Text(it, color = MaterialTheme.colorScheme.error) } - }, - shape = RoundedCornerShape(12.dp), + supportingText = validationError, ) Spacer(Modifier.height(20.dp)) @@ -779,47 +760,30 @@ private fun PickAssetStep( Spacer(Modifier.height(12.dp)) - OutlinedTextField( + val filterSupporting = when { + filterError != null -> stringResource(Res.string.asset_filter_invalid) + visibleAssets.isEmpty() && filterValue.isNotBlank() -> + stringResource(Res.string.asset_filter_no_match) + filterValue.isNotBlank() -> + pluralStringResource( + Res.plurals.asset_filter_visible_count, + allAssets.size, + visibleAssets.size, + allAssets.size, + ) + else -> stringResource(Res.string.asset_filter_help) + } + GhsTextField( value = filterValue, onValueChange = onFilterChanged, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(Res.string.asset_filter_label)) }, - placeholder = { Text(stringResource(Res.string.asset_filter_placeholder)) }, - leadingIcon = { - Icon( - imageVector = Icons.Default.FilterAlt, - contentDescription = null, - ) - }, + label = stringResource(Res.string.asset_filter_label), + placeholder = stringResource(Res.string.asset_filter_placeholder), + leadingIcon = Icons.Default.FilterAlt, singleLine = true, isError = filterError != null, - supportingText = { - Text( - text = - when { - filterError != null -> stringResource(Res.string.asset_filter_invalid) - visibleAssets.isEmpty() && filterValue.isNotBlank() -> - stringResource(Res.string.asset_filter_no_match) - filterValue.isNotBlank() -> - - pluralStringResource( - Res.plurals.asset_filter_visible_count, - allAssets.size, - visibleAssets.size, - allAssets.size, - ) - else -> stringResource(Res.string.asset_filter_help) - }, - color = - if (filterError != null) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - }, + supportingText = filterSupporting, enabled = !isProcessing, - shape = RoundedCornerShape(12.dp), ) Spacer(Modifier.height(8.dp)) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt index a9dbd06ef..73943b895 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt @@ -45,6 +45,7 @@ fun RepoSearchOverride( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Box { + // TODO(ghs-text-field): needs keyboardActions support OutlinedTextField( value = query, onValueChange = onQueryChange, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt index a4a851670..40b2a9e58 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt @@ -30,8 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,6 +41,7 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.apps.presentation.starred.components.StarredCandidateRow +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.navigate_back @@ -165,18 +164,13 @@ private fun ContentBody( } Spacer(Modifier.height(12.dp)) - TextField( + GhsTextField( value = state.searchQuery, onValueChange = { onAction(StarredPickerAction.OnSearchChange(it)) }, modifier = Modifier.fillMaxWidth(), - placeholder = { Text(stringResource(Res.string.starred_picker_search_hint)) }, - leadingIcon = { Icon(imageVector = Icons.Filled.Search, contentDescription = null) }, + placeholder = stringResource(Res.string.starred_picker_search_hint), + leadingIcon = Icons.Filled.Search, singleLine = true, - shape = RoundedCornerShape(16.dp), - colors = TextFieldDefaults.colors( - focusedIndicatorColor = androidx.compose.ui.graphics.Color.Transparent, - unfocusedIndicatorColor = androidx.compose.ui.graphics.Color.Transparent, - ), ) Spacer(Modifier.height(8.dp)) diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 2c07abdf3..9726bd8d9 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -43,8 +43,6 @@ import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -55,8 +53,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue import androidx.compose.material3.Surface @@ -78,8 +74,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -92,6 +86,9 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.auth.presentation.model.AuthLoginState import zed.rainxch.auth.presentation.model.GithubDeviceStartUi +import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon +import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents @@ -110,13 +107,11 @@ import zed.rainxch.githubstore.core.presentation.res.more_requests import zed.rainxch.githubstore.core.presentation.res.more_requests_description import zed.rainxch.githubstore.core.presentation.res.open_github import zed.rainxch.githubstore.core.presentation.res.pat_cancel -import zed.rainxch.githubstore.core.presentation.res.pat_hide import zed.rainxch.githubstore.core.presentation.res.pat_input_label import zed.rainxch.githubstore.core.presentation.res.pat_input_placeholder import zed.rainxch.githubstore.core.presentation.res.pat_open_settings import zed.rainxch.githubstore.core.presentation.res.pat_sheet_description import zed.rainxch.githubstore.core.presentation.res.pat_sheet_title -import zed.rainxch.githubstore.core.presentation.res.pat_show import zed.rainxch.githubstore.core.presentation.res.pat_submit import zed.rainxch.githubstore.core.presentation.res.pat_use_token_instead import zed.rainxch.githubstore.core.presentation.res.redirecting_message @@ -801,13 +796,13 @@ private fun PatSignInSheet( onClick = { onAction(AuthenticationAction.OpenPatSettingsPage) }, ) - OutlinedTextField( + GhsTextField( value = input, onValueChange = { onAction(AuthenticationAction.OnPatInputChanged(it)) }, - label = { Text(stringResource(Res.string.pat_input_label)) }, - placeholder = { Text(stringResource(Res.string.pat_input_placeholder)) }, + label = stringResource(Res.string.pat_input_label), + placeholder = stringResource(Res.string.pat_input_placeholder), singleLine = true, - visualTransformation = if (isMasked) PasswordVisualTransformation() else VisualTransformation.None, + visualTransformation = passwordVisualTransformation(!isMasked), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, autoCorrectEnabled = false, @@ -815,27 +810,11 @@ private fun PatSignInSheet( ), isError = error != null, enabled = !isSubmitting, - shape = RoundedCornerShape(14.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, - ), trailingIcon = { - IconButton( - onClick = { isMasked = !isMasked }, - modifier = Modifier - .size(36.dp) - .clip(CircleShape), - ) { - Icon( - imageVector = if (isMasked) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = stringResource( - if (isMasked) Res.string.pat_show else Res.string.pat_hide, - ), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } + GhsPasswordVisibilityIcon( + visible = !isMasked, + onToggle = { isMasked = !isMasked }, + ) }, modifier = Modifier.fillMaxWidth(), ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt index c8f73b1dd..28eca32b2 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt @@ -24,9 +24,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.core.presentation.vocabulary.Squiggle -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -85,15 +85,12 @@ fun LanguagePicker( Squiggle() Spacer(Modifier.height(8.dp)) - OutlinedTextField( + GhsTextField( value = searchQuery, onValueChange = { searchQuery = it }, - placeholder = { Text(stringResource(Res.string.search_language)) }, - leadingIcon = { - Icon(Icons.Default.Search, contentDescription = null) - }, + placeholder = stringResource(Res.string.search_language), + leadingIcon = Icons.Default.Search, singleLine = true, - shape = RoundedCornerShape(12.dp), modifier = Modifier .fillMaxWidth() diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt index bcdbce52b..03ee5bffb 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SecondaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text @@ -37,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.devprofile.domain.model.RepoFilterType import zed.rainxch.devprofile.domain.model.RepoSortType import zed.rainxch.devprofile.presentation.DeveloperProfileAction @@ -56,27 +56,14 @@ fun FilterSortControls( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - OutlinedTextField( + GhsTextField( value = searchQuery, onValueChange = { query -> onAction(DeveloperProfileAction.OnSearchQueryChange(query)) }, modifier = Modifier.fillMaxWidth(), - placeholder = { - Text( - text = stringResource(Res.string.search_repositories), - maxLines = 1, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = stringResource(Res.string.search_repositories), - modifier = Modifier.size(20.dp), - ) - }, + placeholder = stringResource(Res.string.search_repositories), + leadingIcon = Icons.Default.Search, trailingIcon = { if (searchQuery.isNotBlank()) { IconButton( @@ -91,7 +78,6 @@ fun FilterSortControls( } }, singleLine = true, - shape = RoundedCornerShape(12.dp), ) Row( diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt index 4d5716578..31540e24b 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt @@ -30,8 +30,6 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -40,7 +38,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -48,6 +45,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.collections.immutable.toImmutableList import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.components.ScrollbarContainer import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled @@ -267,15 +265,15 @@ private fun FavouritesSearchBar( query: String, onQueryChange: (String) -> Unit, ) { - TextField( + GhsTextField( value = query, onValueChange = onQueryChange, modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 8.dp), - placeholder = { Text(stringResource(Res.string.search_repositories_hint)) }, - leadingIcon = { Icon(imageVector = Icons.Filled.Search, contentDescription = null) }, + placeholder = stringResource(Res.string.search_repositories_hint), + leadingIcon = Icons.Filled.Search, trailingIcon = { if (query.isNotEmpty()) { IconButton(onClick = { onQueryChange("") }) { @@ -287,12 +285,6 @@ private fun FavouritesSearchBar( } }, singleLine = true, - shape = RoundedCornerShape(16.dp), - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), ) } diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt index dcaffdc9e..26dc244d8 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt @@ -38,8 +38,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -50,7 +48,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -63,6 +60,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.arrowKeyScroll @@ -382,15 +380,15 @@ private fun StarredSearchBar( query: String, onQueryChange: (String) -> Unit, ) { - TextField( + GhsTextField( value = query, onValueChange = onQueryChange, modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 8.dp), - placeholder = { Text(stringResource(Res.string.search_repositories_hint)) }, - leadingIcon = { Icon(imageVector = Icons.Filled.Search, contentDescription = null) }, + placeholder = stringResource(Res.string.search_repositories_hint), + leadingIcon = Icons.Filled.Search, trailingIcon = { if (query.isNotEmpty()) { IconButton(onClick = { onQueryChange("") }) { @@ -402,12 +400,6 @@ private fun StarredSearchBar( } }, singleLine = true, - shape = RoundedCornerShape(16.dp), - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt index 92ceb8c73..d455e9fb8 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt @@ -17,13 +17,13 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.InputChip import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState @@ -62,10 +62,10 @@ fun CustomForgesDialog( verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedTextField( + GhsTextField( value = state.customForgeDraft, onValueChange = { onAction(TweaksAction.OnCustomForgeDraftChanged(it)) }, - placeholder = { Text("forgejo.example.com") }, + placeholder = "forgejo.example.com", singleLine = true, isError = state.customForgeError != null, modifier = Modifier.weight(1f), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index 2d8a52be5..1166c1212 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -55,6 +55,7 @@ import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.model.RootAvailability import zed.rainxch.core.domain.model.ShizukuAvailability import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState @@ -212,19 +213,18 @@ private fun CustomInstallerEditor( onAction: (TweaksAction) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - androidx.compose.material3.OutlinedTextField( + val customSupporting = state.installerAttributionCustomError?.let { + stringResource(Res.string.installer_attribution_custom_error) + } + GhsTextField( value = state.installerAttributionCustomDraft, onValueChange = { onAction(TweaksAction.OnInstallerAttributionCustomChanged(it)) }, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(Res.string.installer_attribution_custom_label)) }, - placeholder = { Text("com.example.installer") }, + label = stringResource(Res.string.installer_attribution_custom_label), + placeholder = "com.example.installer", singleLine = true, isError = state.installerAttributionCustomError != null, - supportingText = state.installerAttributionCustomError?.let { - { - Text(stringResource(Res.string.installer_attribution_custom_error)) - } - }, + supportingText = customSupporting, ) FilledTonalButton( onClick = { onAction(TweaksAction.OnInstallerAttributionCustomSave) }, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt index 36facc4f1..28e5f0683 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt @@ -24,8 +24,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -33,9 +31,7 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -48,14 +44,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.foundation.text.KeyboardOptions import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.SupportedTranslationLanguages import zed.rainxch.core.domain.model.TranslationProvider +import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon +import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState @@ -377,50 +374,28 @@ private fun YoudaoCredentialsForm( color = MaterialTheme.colorScheme.onSurfaceVariant, ) - OutlinedTextField( + GhsTextField( value = state.youdaoAppKey, onValueChange = { onAction(TweaksAction.OnYoudaoAppKeyChanged(it)) }, - label = { Text(stringResource(Res.string.translation_youdao_app_key)) }, + label = stringResource(Res.string.translation_youdao_app_key), singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) - OutlinedTextField( + GhsTextField( value = state.youdaoAppSecret, onValueChange = { onAction(TweaksAction.OnYoudaoAppSecretChanged(it)) }, - label = { Text(stringResource(Res.string.translation_youdao_app_secret)) }, + label = stringResource(Res.string.translation_youdao_app_secret), singleLine = true, - visualTransformation = - if (state.isYoudaoAppSecretVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + visualTransformation = passwordVisualTransformation(state.isYoudaoAppSecretVisible), trailingIcon = { - IconButton( - onClick = { onAction(TweaksAction.OnYoudaoAppSecretVisibilityToggle) }, - ) { - Icon( - imageVector = - if (state.isYoudaoAppSecretVisible) { - Icons.Default.VisibilityOff - } else { - Icons.Default.Visibility - }, - contentDescription = - if (state.isYoudaoAppSecretVisible) { - stringResource(Res.string.proxy_hide_password) - } else { - stringResource(Res.string.proxy_show_password) - }, - modifier = Modifier.size(20.dp), - ) - } + GhsPasswordVisibilityIcon( + visible = state.isYoudaoAppSecretVisible, + onToggle = { onAction(TweaksAction.OnYoudaoAppSecretVisibilityToggle) }, + ) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) Row( @@ -461,52 +436,30 @@ private fun LibreTranslateCredentialsForm( color = MaterialTheme.colorScheme.onSurfaceVariant, ) - OutlinedTextField( + GhsTextField( value = state.libreTranslateBaseUrl, onValueChange = { onAction(TweaksAction.OnLibreTranslateBaseUrlChanged(it)) }, - label = { Text(stringResource(Res.string.translation_libre_base_url)) }, - placeholder = { Text("https://translate.disroot.org") }, + label = stringResource(Res.string.translation_libre_base_url), + placeholder = "https://translate.disroot.org", singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) - OutlinedTextField( + GhsTextField( value = state.libreTranslateApiKey, onValueChange = { onAction(TweaksAction.OnLibreTranslateApiKeyChanged(it)) }, - label = { Text(stringResource(Res.string.translation_libre_api_key)) }, + label = stringResource(Res.string.translation_libre_api_key), singleLine = true, - visualTransformation = - if (state.isLibreTranslateApiKeyVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + visualTransformation = passwordVisualTransformation(state.isLibreTranslateApiKeyVisible), trailingIcon = { - IconButton( - onClick = { onAction(TweaksAction.OnLibreTranslateApiKeyVisibilityToggle) }, - ) { - Icon( - imageVector = - if (state.isLibreTranslateApiKeyVisible) { - Icons.Default.VisibilityOff - } else { - Icons.Default.Visibility - }, - contentDescription = - if (state.isLibreTranslateApiKeyVisible) { - stringResource(Res.string.proxy_hide_password) - } else { - stringResource(Res.string.proxy_show_password) - }, - modifier = Modifier.size(20.dp), - ) - } + GhsPasswordVisibilityIcon( + visible = state.isLibreTranslateApiKeyVisible, + onToggle = { onAction(TweaksAction.OnLibreTranslateApiKeyVisibilityToggle) }, + ) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) Row( @@ -554,41 +507,20 @@ private fun DeeplCredentialsForm( Text(stringResource(Res.string.translation_deepl_get_free_key)) } - OutlinedTextField( + GhsTextField( value = state.deeplAuthKey, onValueChange = { onAction(TweaksAction.OnDeeplAuthKeyChanged(it)) }, - label = { Text(stringResource(Res.string.translation_deepl_auth_key)) }, + label = stringResource(Res.string.translation_deepl_auth_key), singleLine = true, - visualTransformation = - if (state.isDeeplAuthKeyVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + visualTransformation = passwordVisualTransformation(state.isDeeplAuthKeyVisible), trailingIcon = { - IconButton( - onClick = { onAction(TweaksAction.OnDeeplAuthKeyVisibilityToggle) }, - ) { - Icon( - imageVector = - if (state.isDeeplAuthKeyVisible) { - Icons.Default.VisibilityOff - } else { - Icons.Default.Visibility - }, - contentDescription = - if (state.isDeeplAuthKeyVisible) { - stringResource(Res.string.proxy_hide_password) - } else { - stringResource(Res.string.proxy_show_password) - }, - modifier = Modifier.size(20.dp), - ) - } + GhsPasswordVisibilityIcon( + visible = state.isDeeplAuthKeyVisible, + onToggle = { onAction(TweaksAction.OnDeeplAuthKeyVisibilityToggle) }, + ) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) Row( @@ -636,51 +568,29 @@ private fun MicrosoftCredentialsForm( Text(stringResource(Res.string.translation_microsoft_get_free_key)) } - OutlinedTextField( + GhsTextField( value = state.microsoftTranslatorKey, onValueChange = { onAction(TweaksAction.OnMicrosoftTranslatorKeyChanged(it)) }, - label = { Text(stringResource(Res.string.translation_microsoft_key)) }, + label = stringResource(Res.string.translation_microsoft_key), singleLine = true, - visualTransformation = - if (state.isMicrosoftTranslatorKeyVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, + visualTransformation = passwordVisualTransformation(state.isMicrosoftTranslatorKeyVisible), trailingIcon = { - IconButton( - onClick = { onAction(TweaksAction.OnMicrosoftTranslatorKeyVisibilityToggle) }, - ) { - Icon( - imageVector = - if (state.isMicrosoftTranslatorKeyVisible) { - Icons.Default.VisibilityOff - } else { - Icons.Default.Visibility - }, - contentDescription = - if (state.isMicrosoftTranslatorKeyVisible) { - stringResource(Res.string.proxy_hide_password) - } else { - stringResource(Res.string.proxy_show_password) - }, - modifier = Modifier.size(20.dp), - ) - } + GhsPasswordVisibilityIcon( + visible = state.isMicrosoftTranslatorKeyVisible, + onToggle = { onAction(TweaksAction.OnMicrosoftTranslatorKeyVisibilityToggle) }, + ) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) - OutlinedTextField( + GhsTextField( value = state.microsoftTranslatorRegion, onValueChange = { onAction(TweaksAction.OnMicrosoftTranslatorRegionChanged(it)) }, - label = { Text(stringResource(Res.string.translation_microsoft_region)) }, - placeholder = { Text("global") }, + label = stringResource(Res.string.translation_microsoft_region), + placeholder = "global", singleLine = true, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), ) Row( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt index de49db7b3..3ea807181 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt @@ -72,6 +72,7 @@ fun ConditionalFields( } } +// TODO(ghs-text-field): needs minLines support @Composable private fun MultilineField( value: String, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt index 3a0283928..bef5a5172 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.feedback_close @@ -108,14 +109,15 @@ fun FeedbackBottomSheet( onSelected = { viewModel.onAction(FeedbackAction.OnTopicChange(it)) }, ) - OutlinedTextField( + GhsTextField( value = state.title, onValueChange = { viewModel.onAction(FeedbackAction.OnTitleChange(it)) }, - label = { Text(stringResource(Res.string.feedback_field_title) + " *") }, + label = stringResource(Res.string.feedback_field_title) + " *", singleLine = true, modifier = Modifier.fillMaxWidth(), ) + // TODO(ghs-text-field): needs minLines support OutlinedTextField( value = state.description, onValueChange = { viewModel.onAction(FeedbackAction.OnDescriptionChange(it)) }, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt index cba2c700f..6b3cfe854 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -63,6 +62,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.ForgeKind import zed.rainxch.core.domain.model.HostToken +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.host_tokens_action_add @@ -570,47 +570,44 @@ private fun AddTokenDialog( text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { if (state.draftForge == null) { - OutlinedTextField( + val hostSupporting = state.draftHostError?.let { stringResource(it) } + ?: state.draftHostNormalized + .takeIf { it.isNotBlank() && it != state.draftHost.trim() } + ?.let { normalized -> + stringResource( + Res.string.host_tokens_compose_will_connect, + normalized, + ) + } + GhsTextField( value = state.draftHost, onValueChange = { onAction(HostTokensAction.OnDraftHostChanged(it)) }, - label = { Text(stringResource(Res.string.host_tokens_compose_field_forge_address)) }, + label = stringResource(Res.string.host_tokens_compose_field_forge_address), singleLine = true, isError = state.draftHostError != null, - supportingText = state.draftHostError?.let { res -> - { Text(stringResource(res)) } - } ?: state.draftHostNormalized - .takeIf { it.isNotBlank() && it != state.draftHost.trim() } - ?.let { normalized -> - { - Text( - stringResource( - Res.string.host_tokens_compose_will_connect, - normalized, - ), - ) - } - }, + supportingText = hostSupporting, ) } - OutlinedTextField( + val tokenSupporting = state.draftTokenError?.let { stringResource(it) } + ?: state.draftDetectedTokenKind?.let { kind -> + stringResource(Res.string.host_tokens_compose_detected, kind) + } + ?: replacingExisting?.let { + stringResource(Res.string.host_tokens_compose_replace_hint) + } + GhsTextField( value = state.draftToken, onValueChange = { onAction(HostTokensAction.OnDraftTokenChanged(it)) }, - label = { Text(stringResource(Res.string.host_tokens_field_token)) }, + label = stringResource(Res.string.host_tokens_field_token), singleLine = true, isError = state.draftTokenError != null, - supportingText = state.draftTokenError?.let { res -> - { Text(stringResource(res)) } - } ?: state.draftDetectedTokenKind?.let { kind -> - { Text(stringResource(Res.string.host_tokens_compose_detected, kind)) } - } ?: replacingExisting?.let { - { Text(stringResource(Res.string.host_tokens_compose_replace_hint)) } - }, + supportingText = tokenSupporting, visualTransformation = PasswordVisualTransformation(), ) - OutlinedTextField( + GhsTextField( value = state.draftDisplayName, onValueChange = { onAction(HostTokensAction.OnDraftDisplayNameChanged(it)) }, - label = { Text(stringResource(Res.string.host_tokens_field_display_name)) }, + label = stringResource(Res.string.host_tokens_field_display_name), singleLine = true, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt index 32c77e9ae..429145f02 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt @@ -4,13 +4,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.cancel import zed.rainxch.githubstore.core.presentation.res.mirror_custom_dialog_hint @@ -30,10 +30,10 @@ fun CustomMirrorDialog( title = { Text(stringResource(Res.string.mirror_custom_dialog_title)) }, text = { Column(modifier = Modifier.fillMaxWidth()) { - OutlinedTextField( + GhsTextField( value = draft, onValueChange = onDraftChange, - placeholder = { Text(stringResource(Res.string.mirror_custom_dialog_hint)) }, + placeholder = stringResource(Res.string.mirror_custom_dialog_hint), isError = error != null, modifier = Modifier.fillMaxWidth(), singleLine = true, From abffd89381905c8117bf01d9265df045f26f57d3 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 21:36:50 +0500 Subject: [PATCH 105/172] feat(core): GhsButton canonical with variants, swap into Connection --- .../components/buttons/GhsButton.kt | 192 ++++++++++++++++++ .../connection/TweaksConnectionRoot.kt | 139 ++++--------- 2 files changed, 228 insertions(+), 103 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt new file mode 100644 index 000000000..fc7f5dbe1 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt @@ -0,0 +1,192 @@ +package zed.rainxch.core.presentation.components.buttons + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape + +enum class GhsButtonVariant { + Primary, + Tonal, + Outline, + Text, + Destructive, +} + +enum class GhsButtonSize { + Sm, + Md, + Lg, +} + +@Composable +fun GhsButton( + onClick: () -> Unit, + label: String, + modifier: Modifier = Modifier, + variant: GhsButtonVariant = GhsButtonVariant.Primary, + size: GhsButtonSize = GhsButtonSize.Md, + enabled: Boolean = true, + loading: Boolean = false, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, +) { + val cs = MaterialTheme.colorScheme + val container: Color = when (variant) { + GhsButtonVariant.Primary -> cs.primary + GhsButtonVariant.Tonal -> cs.secondaryContainer + GhsButtonVariant.Outline -> Color.Transparent + GhsButtonVariant.Text -> Color.Transparent + GhsButtonVariant.Destructive -> cs.errorContainer + } + val content: Color = when (variant) { + GhsButtonVariant.Primary -> cs.onPrimary + GhsButtonVariant.Tonal -> cs.onSecondaryContainer + GhsButtonVariant.Outline -> cs.onSurface + GhsButtonVariant.Text -> cs.primary + GhsButtonVariant.Destructive -> cs.onErrorContainer + } + val borderColor: Color? = when (variant) { + GhsButtonVariant.Outline -> cs.outline + else -> null + } + val shape: Shape = when (variant) { + GhsButtonVariant.Primary -> WonkySquircleShape.CtaPrimary + GhsButtonVariant.Destructive -> WonkySquircleShape.CtaPrimary + GhsButtonVariant.Tonal -> RoundedCornerShape(50) + GhsButtonVariant.Outline -> RoundedCornerShape(50) + GhsButtonVariant.Text -> RoundedCornerShape(50) + } + val minHeight: Dp = when (size) { + GhsButtonSize.Sm -> 36.dp + GhsButtonSize.Md -> 44.dp + GhsButtonSize.Lg -> 52.dp + } + val hPadding: Dp = when (size) { + GhsButtonSize.Sm -> 12.dp + GhsButtonSize.Md -> 18.dp + GhsButtonSize.Lg -> 22.dp + } + val vPadding: Dp = when (size) { + GhsButtonSize.Sm -> 6.dp + GhsButtonSize.Md -> 10.dp + GhsButtonSize.Lg -> 14.dp + } + val fontSize = when (size) { + GhsButtonSize.Sm -> 13.sp + GhsButtonSize.Md -> 14.sp + GhsButtonSize.Lg -> 16.sp + } + val iconSize = when (size) { + GhsButtonSize.Sm -> 16.dp + GhsButtonSize.Md -> 18.dp + GhsButtonSize.Lg -> 20.dp + } + + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed && enabled && !loading) 0.97f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessHigh, + ), + label = "ghs-button-press", + ) + val alpha = if (enabled && !loading) 1f else 0.38f + + var rowModifier = modifier + .scale(scale) + .clip(shape) + .background(container.copy(alpha = container.alpha * alpha)) + if (borderColor != null) { + rowModifier = rowModifier.border( + width = 1.dp, + color = borderColor.copy(alpha = alpha), + shape = shape, + ) + } + rowModifier = rowModifier + .clickable( + enabled = enabled && !loading, + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + .heightIn(min = minHeight) + .padding(horizontal = hPadding, vertical = vPadding) + + Row( + modifier = rowModifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider(LocalContentColor provides content.copy(alpha = alpha)) { + if (loading) { + CircularProgressIndicator( + modifier = Modifier.size(iconSize), + strokeWidth = 2.dp, + color = content.copy(alpha = alpha), + ) + } else if (leadingIcon != null) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + modifier = Modifier.size(iconSize), + tint = content.copy(alpha = alpha), + ) + } + Text( + text = label, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = fontSize, + ), + color = content.copy(alpha = alpha), + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) + if (trailingIcon != null && !loading) { + Icon( + imageVector = trailingIcon, + contentDescription = null, + modifier = Modifier.size(iconSize), + tint = content.copy(alpha = alpha), + ) + } + } + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index 6e01df8c2..bbb2b88d2 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -20,12 +20,8 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.NetworkCheck import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -47,10 +43,11 @@ import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.ProxyScope +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation -import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res @@ -204,44 +201,22 @@ private fun MainConnectionCard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedButton( + GhsButton( onClick = { onAction(TweaksAction.OnMasterProxyTest) }, - shape = Radii.chip, - modifier = Modifier.weight(1f), + label = "Test", + variant = GhsButtonVariant.Outline, + leadingIcon = Icons.Default.NetworkCheck, enabled = !form.isTestInProgress, - ) { - if (form.isTestInProgress) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - ) - } else { - Icon( - imageVector = Icons.Default.NetworkCheck, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - Spacer(Modifier.size(6.dp)) - Text(text = "Test") - } - FilledTonalButton( + loading = form.isTestInProgress, + modifier = Modifier.weight(1f), + ) + GhsButton( onClick = { onAction(TweaksAction.OnMasterProxySave) }, - shape = WonkySquircleShape.CtaPrimary, + label = stringResource(Res.string.proxy_save), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Default.Save, modifier = Modifier.weight(1f), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(6.dp)) - Text(text = stringResource(Res.string.proxy_save)) - } + ) } } } @@ -249,23 +224,13 @@ private fun MainConnectionCard( AnimatedVisibility(visible = form.type == ProxyType.NONE || form.type == ProxyType.SYSTEM) { Column { Spacer(Modifier.height(12.dp)) - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnMasterProxySave) }, - shape = WonkySquircleShape.CtaPrimary, + label = stringResource(Res.string.proxy_save), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Default.Save, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(6.dp)) - Text(text = stringResource(Res.string.proxy_save)) - } + ) } } } @@ -509,44 +474,22 @@ private fun ScopeOverrideRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedButton( + GhsButton( onClick = { onAction(TweaksAction.OnProxyTest(scope)) }, - shape = Radii.chip, - modifier = Modifier.weight(1f), + label = "Test", + variant = GhsButtonVariant.Outline, + leadingIcon = Icons.Default.NetworkCheck, enabled = !scopeForm.isTestInProgress, - ) { - if (scopeForm.isTestInProgress) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - ) - } else { - Icon( - imageVector = Icons.Default.NetworkCheck, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - Spacer(Modifier.size(6.dp)) - Text(text = "Test") - } - FilledTonalButton( + loading = scopeForm.isTestInProgress, + modifier = Modifier.weight(1f), + ) + GhsButton( onClick = { onAction(TweaksAction.OnProxySave(scope)) }, - shape = RoundedCornerShape(50), + label = stringResource(Res.string.proxy_save), + variant = GhsButtonVariant.Tonal, + leadingIcon = Icons.Default.Save, modifier = Modifier.weight(1f), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(6.dp)) - Text(text = stringResource(Res.string.proxy_save)) - } + ) } } } @@ -557,23 +500,13 @@ private fun ScopeOverrideRow( ) { Column { Spacer(Modifier.height(12.dp)) - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnProxySave(scope)) }, - shape = WonkySquircleShape.CtaPrimary, + label = stringResource(Res.string.proxy_save), + variant = GhsButtonVariant.Tonal, + leadingIcon = Icons.Default.Save, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(6.dp)) - Text(text = stringResource(Res.string.proxy_save)) - } + ) } } } From 0a0779cce078252e95459a33d98700689758c3d9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:09:46 +0500 Subject: [PATCH 106/172] refactor(ui): swap Material3 buttons for GhsButton across app --- .../app/components/RateLimitDialog.kt | 44 +++-- .../app/components/SessionExpiredDialog.kt | 31 ++-- .../app/onboarding/OnboardingScreen.kt | 35 ++-- .../announcements/AnnouncementCard.kt | 55 ++++--- .../CriticalAnnouncementModal.kt | 29 ++-- .../components/overlays/GhsConfirmDialog.kt | 21 ++- .../components/whatsnew/WhatsNewSheet.kt | 25 ++- .../zed/rainxch/apps/presentation/AppsRoot.kt | 150 ++++++------------ .../AdvancedAppSettingsBottomSheet.kt | 44 +++-- .../presentation/components/AppDetailPane.kt | 131 +++++---------- .../presentation/components/CompactAppRow.kt | 26 ++- .../components/ImportSummarySheet.kt | 29 ++-- .../apps/presentation/components/KaoBanner.kt | 13 +- .../components/LinkAppBottomSheet.kt | 52 +++--- .../presentation/components/UpdatesBanner.kt | 31 ++-- .../components/VariantPickerDialog.kt | 22 ++- .../components/AutoImportSummaryScreen.kt | 20 ++- .../import/components/CandidateCard.kt | 47 +++--- .../import/components/CompletionToast.kt | 11 +- .../import/components/EmptyStateScreen.kt | 45 +++--- .../import/components/ImportProgressScreen.kt | 14 +- .../import/components/ImportProposalBanner.kt | 15 +- .../components/PermissionRationaleScreen.kt | 38 ++--- .../import/components/WizardList.kt | 22 +-- .../presentation/starred/StarredPickerRoot.kt | 16 +- .../auth/presentation/AuthenticationRoot.kt | 82 +++++----- .../details/presentation/DetailsRoot.kt | 110 +++++++------ .../components/InspectApkButton.kt | 1 + .../components/LinkedRepoBanner.kt | 20 +-- .../components/ReleaseAssetsPicker.kt | 12 +- .../components/TranslationCard.kt | 86 +++------- .../components/sections/ReleaseChannel.kt | 1 + .../components/states/ErrorState.kt | 32 ++-- .../presentation/DeveloperProfileRoot.kt | 15 +- .../zed/rainxch/home/presentation/HomeRoot.kt | 11 +- .../presentation/components/StarredRowItem.kt | 26 +-- .../components/TrendingRowItem.kt | 41 ++--- .../presentation/components/LogoutDialog.kt | 41 ++--- .../components/sections/AccountSection.kt | 29 ++-- .../rainxch/search/presentation/SearchRoot.kt | 33 ++-- .../components/SearchFiltersSheet.kt | 46 ++---- .../components/SearchHistorySection.kt | 16 +- .../components/SortByBottomSheet.kt | 13 +- .../starred/presentation/StarredReposRoot.kt | 15 +- .../components/ClearDownloadsDialog.kt | 37 ++--- .../components/CustomForgesDialog.kt | 22 ++- .../presentation/components/RestartBanner.kt | 33 ++-- .../components/sections/Installation.kt | 73 ++++----- .../components/sections/Translation.kt | 82 ++++------ .../feedback/components/SendActions.kt | 42 ++--- .../hidden/HiddenRepositoriesRoot.kt | 22 ++- .../presentation/hosttokens/HostTokensRoot.kt | 56 ++++--- .../presentation/mirror/MirrorPickerRoot.kt | 20 +-- .../mirror/components/CustomMirrorDialog.kt | 22 ++- .../skipped/SkippedUpdatesRoot.kt | 13 +- .../presentation/storage/TweaksStorageRoot.kt | 26 +-- 56 files changed, 892 insertions(+), 1152 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt index d96572e82..01274a6e5 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/RateLimitDialog.kt @@ -7,11 +7,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -20,6 +18,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.RateLimitInfo +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.rate_limit_close @@ -105,31 +106,28 @@ fun RateLimitDialog( }, confirmButton = { if (!isAuthenticated) { - Button(onClick = onSignIn) { - Text( - text = stringResource(Res.string.rate_limit_sign_in), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary, - ) - } + GhsButton( + onClick = onSignIn, + label = stringResource(Res.string.rate_limit_sign_in), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) } else { - Button(onClick = onDismiss) { - Text( - text = stringResource(Res.string.rate_limit_ok), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - ) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.rate_limit_ok), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) } }, dismissButton = { - TextButton(onClick = onDismiss) { - Text( - text = stringResource(Res.string.rate_limit_close), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - ) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.rate_limit_close), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/SessionExpiredDialog.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/SessionExpiredDialog.kt index f666db85e..905290941 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/SessionExpiredDialog.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/components/SessionExpiredDialog.kt @@ -5,15 +5,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.* @Composable @@ -56,22 +57,20 @@ fun SessionExpiredDialog( } }, confirmButton = { - Button(onClick = onSignIn) { - Text( - text = stringResource(Res.string.sign_in_again), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary, - ) - } + GhsButton( + onClick = onSignIn, + label = stringResource(Res.string.sign_in_again), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton(onClick = onDismiss) { - Text( - text = stringResource(Res.string.continue_as_guest), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - ) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.continue_as_guest), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index cf4ebf74e..ff3a1dd51 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -43,9 +43,8 @@ import androidx.compose.ui.unit.sp import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ThemeMode -import zed.rainxch.core.presentation.components.buttons.OutlineButton -import zed.rainxch.core.presentation.components.buttons.PrimaryButton -import zed.rainxch.core.presentation.components.buttons.TintedButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.fraunces import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents @@ -294,9 +293,11 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) - PrimaryButton(onClick = { onAction(OnboardingAction.OnSignInClick) }) { - Text("Sign in") - } + GhsButton( + onClick = { onAction(OnboardingAction.OnSignInClick) }, + label = "Sign in", + variant = GhsButtonVariant.Primary, + ) } } @@ -405,7 +406,11 @@ private fun PermissionRow( ) } } else { - TintedButton(onClick = onAllowClick) { Text("Allow") } + GhsButton( + onClick = onAllowClick, + label = "Allow", + variant = GhsButtonVariant.Tonal, + ) } } } @@ -421,14 +426,18 @@ private fun ActionRow( verticalAlignment = Alignment.CenterVertically, ) { if (state.currentStep == OnboardingStep.SIGN_IN || state.currentStep == OnboardingStep.PERMISSIONS) { - OutlineButton(onClick = { onAction(OnboardingAction.OnSkipStepClick) }) { - Text("Skip") - } + GhsButton( + onClick = { onAction(OnboardingAction.OnSkipStepClick) }, + label = "Skip", + variant = GhsButtonVariant.Outline, + ) } else { Spacer(Modifier.size(80.dp)) } - PrimaryButton(onClick = { onAction(OnboardingAction.OnNextClick) }) { - Text(if (state.isLast) "Get started" else "Next") - } + GhsButton( + onClick = { onAction(OnboardingAction.OnNextClick) }, + label = if (state.isLast) "Get started" else "Next", + variant = GhsButtonVariant.Primary, + ) } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementCard.kt index 958693b96..5aa5078a7 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementCard.kt @@ -25,7 +25,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -189,13 +191,12 @@ private fun ExpandableBody(body: String) { }, ) if (!expanded && isOverflowing) { - TextButton(onClick = { expanded = true }) { - Text( - text = stringResource(Res.string.announcements_read_more), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, - ) - } + GhsButton( + onClick = { expanded = true }, + label = stringResource(Res.string.announcements_read_more), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } @@ -214,27 +215,31 @@ private fun ActionRow( verticalAlignment = Alignment.CenterVertically, ) { if (!announcement.ctaUrl.isNullOrBlank()) { - TextButton(onClick = onCtaClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Spacer(Modifier.width(4.dp)) - val resolvedLabel = announcement.ctaLabel?.takeIf { it.isNotBlank() } - ?: stringResource(Res.string.announcements_read_more) - Text(text = resolvedLabel) - } + val resolvedLabel = announcement.ctaLabel?.takeIf { it.isNotBlank() } + ?: stringResource(Res.string.announcements_read_more) + GhsButton( + onClick = onCtaClick, + label = resolvedLabel, + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + leadingIcon = Icons.AutoMirrored.Filled.OpenInNew, + ) } Spacer(Modifier.weight(1f)) if (announcement.requiresAcknowledgment && !isAcknowledged) { - TextButton(onClick = onAcknowledgeClick) { - Text(text = stringResource(Res.string.announcements_acknowledge)) - } + GhsButton( + onClick = onAcknowledgeClick, + label = stringResource(Res.string.announcements_acknowledge), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } else if (announcement.dismissible) { - TextButton(onClick = onDismissClick) { - Text(text = stringResource(Res.string.dismiss)) - } + GhsButton( + onClick = onDismissClick, + label = stringResource(Res.string.dismiss), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt index 28345f346..17cb1ace7 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/CriticalAnnouncementModal.kt @@ -10,12 +10,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Security import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -65,30 +64,24 @@ fun CriticalAnnouncementModal( } }, confirmButton = { - Button( + GhsButton( onClick = onAcknowledge, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), + label = stringResource(Res.string.announcements_acknowledge), + variant = GhsButtonVariant.Destructive, modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(Res.string.announcements_acknowledge)) - } + ) }, dismissButton = if (!announcement.ctaUrl.isNullOrBlank()) { { - TextButton( + GhsButton( onClick = onOpenDetails, + label = announcement.ctaLabel + ?: stringResource(Res.string.announcements_view_details), + variant = GhsButtonVariant.Text, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), - ) { - Text( - text = announcement.ctaLabel - ?: stringResource(Res.string.announcements_view_details), - ) - } + ) } } else { null diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt index fc32162ea..e13480561 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt @@ -14,14 +14,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import zed.rainxch.core.presentation.components.buttons.OutlineButton -import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.vocabulary.Squiggle @@ -71,11 +71,18 @@ fun GhsConfirmDialog( horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End), verticalAlignment = Alignment.CenterVertically, ) { - OutlineButton(onClick = onDismiss) { Text(cancelLabel) } - PrimaryButton( + GhsButton( + onClick = onDismiss, + label = cancelLabel, + variant = GhsButtonVariant.Outline, + size = GhsButtonSize.Sm, + ) + GhsButton( onClick = onConfirm, - backgroundColor = if (destructive) cs.error else cs.primary, - ) { Text(confirmLabel) } + label = confirmLabel, + variant = if (destructive) GhsButtonVariant.Destructive else GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) } } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt index 007a08d39..5caaac6d3 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt @@ -11,14 +11,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -71,24 +70,20 @@ fun WhatsNewSheet( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Button( + GhsButton( onClick = onDismiss, + label = stringResource(Res.string.whats_new_cta_dismiss), + variant = GhsButtonVariant.Primary, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text(text = stringResource(Res.string.whats_new_cta_dismiss)) - } + ) if (showHistoryAction) { - TextButton( + GhsButton( onClick = onViewHistory, + label = stringResource(Res.string.whats_new_cta_history), + variant = GhsButtonVariant.Text, modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(Res.string.whats_new_cta_history)) - } + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index 69b2b6cd2..6cd917d5f 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -44,8 +44,6 @@ import androidx.compose.material.icons.outlined.FileUpload import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator @@ -62,7 +60,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable @@ -107,6 +104,9 @@ import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.apps.presentation.model.UpdateState import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled @@ -486,21 +486,20 @@ fun AppsScreen( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { onAction(AppsAction.OnUninstallConfirmed(app)) }, - ) { - Text( - text = stringResource(Res.string.uninstall), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.uninstall), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { onAction(AppsAction.OnDismissUninstallDialog) }, - ) { - Text(text = stringResource(Res.string.cancel)) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -523,21 +522,20 @@ fun AppsScreen( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { onAction(AppsAction.OnConfirmDiscardPendingInstall(app)) }, - ) { - Text( - text = stringResource(Res.string.discard_pending_install), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.discard_pending_install), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { onAction(AppsAction.OnDismissDiscardPendingDialog) }, - ) { - Text(text = stringResource(Res.string.cancel)) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -1339,47 +1337,26 @@ fun AppItemCard( when (appItem.updateState) { is UpdateState.Downloading, is UpdateState.Installing, is UpdateState.CheckingUpdate -> { - Button( + GhsButton( onClick = onCancelClick, + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Destructive, + leadingIcon = Icons.Default.Cancel, modifier = Modifier.weight(1f), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = stringResource(Res.string.cancel), - modifier = Modifier.size(18.dp), - ) - - Spacer(Modifier.width(4.dp)) - - Text( - text = stringResource(Res.string.cancel), - ) - } + ) } else -> { if (app.pendingInstallFilePath != null) { - Button( + GhsButton( onClick = onInstallPendingClick, - modifier = Modifier.weight(1f), + label = stringResource(Res.string.install), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Default.Update, enabled = !isBusy, - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.install), - ) - } + modifier = Modifier.weight(1f), + ) IconButton(onClick = onDiscardPendingClick) { Icon( @@ -1389,58 +1366,31 @@ fun AppItemCard( ) } } else if (app.isUpdateAvailable && !app.isPendingInstall) { - Button( + GhsButton( onClick = onUpdateClick, + label = stringResource(Res.string.update), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Default.Update, modifier = Modifier.weight(1f), - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.update), - ) - } + ) } else if (app.isPendingInstall) { - Button( + GhsButton( onClick = onDiscardPendingClick, + label = stringResource(Res.string.discard_pending_install), + variant = GhsButtonVariant.Destructive, + leadingIcon = Icons.Default.Cancel, modifier = Modifier.weight(1f), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.discard_pending_install), - ) - } + ) } else { - Button( - shapes = ButtonDefaults.shapes(), + GhsButton( onClick = onOpenClick, - modifier = Modifier.weight(1f), + label = stringResource(Res.string.open), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.AutoMirrored.Filled.OpenInNew, enabled = !isBusy, - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.open), - ) - } + modifier = Modifier.weight(1f), + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt index a61859f32..8e5f5b421 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AdvancedAppSettingsBottomSheet.kt @@ -25,17 +25,17 @@ import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -114,9 +114,12 @@ fun AdvancedAppSettingsBottomSheet( leadingIcon = Icons.Default.FilterAlt, trailingIcon = { if (state.advancedFilterDraft.isNotEmpty()) { - TextButton(onClick = { onAction(AppsAction.OnAdvancedClearFilter) }) { - Text(stringResource(Res.string.clear)) - } + GhsButton( + onClick = { onAction(AppsAction.OnAdvancedClearFilter) }, + label = stringResource(Res.string.clear), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } }, singleLine = true, @@ -190,32 +193,21 @@ fun AdvancedAppSettingsBottomSheet( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - OutlinedButton( + GhsButton( onClick = { onAction(AppsAction.OnDismissAdvancedSettings) }, + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Outline, enabled = !state.advancedSavingFilter, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(12.dp), - ) { - Text(stringResource(Res.string.cancel)) - } - FilledTonalButton( + ) + GhsButton( onClick = { onAction(AppsAction.OnAdvancedSaveFilter) }, + label = stringResource(Res.string.advanced_save), + variant = GhsButtonVariant.Tonal, enabled = !state.advancedSavingFilter && state.advancedFilterError == null, + loading = state.advancedSavingFilter, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(12.dp), - ) { - if (state.advancedSavingFilter) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - ) - Spacer(Modifier.width(8.dp)) - } - Text( - text = stringResource(Res.string.advanced_save), - fontWeight = FontWeight.Bold, - ) - } + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt index ef71f75cf..462df2521 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/AppDetailPane.kt @@ -27,13 +27,10 @@ import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -48,6 +45,8 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.apps.presentation.model.AppItem import zed.rainxch.apps.presentation.model.UpdateState +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_pending_install import zed.rainxch.githubstore.core.presentation.res.apps_compact_status_pre_release_on @@ -133,37 +132,20 @@ fun AppDetailPane( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - OutlinedButton( + GhsButton( onClick = onOpenRepo, - modifier = Modifier.weight(1f).height(44.dp), - shape = RoundedCornerShape(50), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.apps_two_pane_open_repo)) - } - OutlinedButton( + label = stringResource(Res.string.apps_two_pane_open_repo), + variant = GhsButtonVariant.Outline, + leadingIcon = Icons.AutoMirrored.Filled.OpenInNew, + modifier = Modifier.weight(1f), + ) + GhsButton( onClick = onUninstall, + label = stringResource(Res.string.uninstall), + variant = GhsButtonVariant.Destructive, enabled = !isBusy, - modifier = Modifier.height(44.dp), - shape = RoundedCornerShape(50), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.55f)), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Icon( - imageVector = Icons.Outlined.DeleteOutline, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.uninstall)) - } + leadingIcon = Icons.Outlined.DeleteOutline, + ) } Spacer(Modifier.height(20.dp)) @@ -389,77 +371,46 @@ private fun PrimaryActionsRow( ) { when (appItem.updateState) { is UpdateState.Downloading, is UpdateState.Installing, is UpdateState.CheckingUpdate -> { - Button( + GhsButton( onClick = onCancelUpdate, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(50), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.cancel), fontWeight = FontWeight.SemiBold) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Destructive, + leadingIcon = Icons.Default.Cancel, + modifier = Modifier.weight(1f), + ) } else -> { if (app.pendingInstallFilePath != null) { - Button( + GhsButton( onClick = onInstallPending, + label = stringResource(Res.string.install), + variant = GhsButtonVariant.Primary, enabled = !isBusy, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(50), - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.install), fontWeight = FontWeight.SemiBold) - } - OutlinedButton( + leadingIcon = Icons.Default.Update, + modifier = Modifier.weight(1f), + ) + GhsButton( onClick = onDiscardPending, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 18.dp), - ) { - Text(stringResource(Res.string.discard_pending_install)) - } + label = stringResource(Res.string.discard_pending_install), + variant = GhsButtonVariant.Outline, + ) } else if (app.isUpdateAvailable && !app.isPendingInstall) { - Button( + GhsButton( onClick = onUpdateApp, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(50), - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.update), fontWeight = FontWeight.SemiBold) - } + label = stringResource(Res.string.update), + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Default.Update, + modifier = Modifier.weight(1f), + ) } else { - Button( + GhsButton( onClick = onOpenApp, + label = stringResource(Res.string.open), + variant = GhsButtonVariant.Primary, enabled = !isBusy, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(50), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(6.dp)) - Text(stringResource(Res.string.open), fontWeight = FontWeight.SemiBold) - } + leadingIcon = Icons.AutoMirrored.Filled.OpenInNew, + modifier = Modifier.weight(1f), + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt index c336c56a6..5940f5724 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt @@ -24,8 +24,9 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -156,23 +157,14 @@ fun CompactAppRow( if (app.pendingInstallFilePath != null) { - Button( + GhsButton( onClick = onInstallPendingClick, + label = stringResource(Res.string.install), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, enabled = !isBusy, - contentPadding = PaddingValues(horizontal = 12.dp), - modifier = Modifier.height(40.dp), - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.install), - style = MaterialTheme.typography.labelMedium, - ) - } + leadingIcon = Icons.Default.Update, + ) Spacer(Modifier.width(4.dp)) } else if (app.isPendingInstall) { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt index 524df7d46..1397fc5c8 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/ImportSummarySheet.kt @@ -20,12 +20,13 @@ import androidx.compose.material.icons.outlined.ExpandLess import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.RemoveCircleOutline import androidx.compose.material.icons.outlined.WarningAmber -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -159,19 +160,14 @@ fun ImportSummarySheet( ) } - Button( + GhsButton( onClick = onDismiss, + label = stringResource(Res.string.import_summary_close), + variant = GhsButtonVariant.Primary, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), - shape = RoundedCornerShape(16.dp), - contentPadding = PaddingValues(vertical = 12.dp), - ) { - Text( - text = stringResource(Res.string.import_summary_close), - fontWeight = FontWeight.SemiBold, - ) - } + ) Spacer(Modifier.height(8.dp)) } } @@ -225,17 +221,12 @@ private fun UnknownFormatSheet( ) } } - Button( + GhsButton( onClick = onDismiss, + label = stringResource(Res.string.import_summary_close), + variant = GhsButtonVariant.Primary, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - contentPadding = PaddingValues(vertical = 12.dp), - ) { - Text( - text = stringResource(Res.string.import_summary_close), - fontWeight = FontWeight.SemiBold, - ) - } + ) Spacer(Modifier.height(8.dp)) } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/KaoBanner.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/KaoBanner.kt index cc1e52a7f..34a99bf8b 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/KaoBanner.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/KaoBanner.kt @@ -17,7 +17,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -87,9 +89,12 @@ fun KaoBanner( modifier = Modifier.fillMaxWidth().padding(end = 8.dp), horizontalArrangement = Arrangement.End, ) { - TextButton(onClick = onLearnMore) { - Text(text = stringResource(Res.string.kao_banner_cta)) - } + GhsButton( + onClick = onLearnMore, + label = stringResource(Res.string.kao_banner_cta), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt index cb2c4b5a3..21038d999 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/LinkAppBottomSheet.kt @@ -28,11 +28,12 @@ import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.Search import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import androidx.compose.material3.Surface @@ -415,13 +416,12 @@ private fun SmartMatchStep( color = MaterialTheme.colorScheme.error, ) Spacer(Modifier.height(8.dp)) - FilledTonalButton( + GhsButton( onClick = onRetry, + label = stringResource(Res.string.retry), + variant = GhsButtonVariant.Tonal, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - ) { - Text(stringResource(Res.string.retry)) - } + ) } suggestions.isEmpty() -> { @@ -463,14 +463,13 @@ private fun SmartMatchStep( Spacer(Modifier.height(12.dp)) - FilledTonalButton( + GhsButton( onClick = onEnterUrlManually, - modifier = Modifier.fillMaxWidth(), + label = stringResource(Res.string.link_smart_search_enter_manually), + variant = GhsButtonVariant.Tonal, enabled = !isValidating, - shape = RoundedCornerShape(12.dp), - ) { - Text(stringResource(Res.string.link_smart_search_enter_manually)) - } + modifier = Modifier.fillMaxWidth(), + ) } } @@ -674,27 +673,18 @@ private fun EnterUrlStep( Spacer(Modifier.height(20.dp)) - FilledTonalButton( + GhsButton( onClick = onConfirm, - modifier = Modifier.fillMaxWidth(), - enabled = repoUrl.isNotBlank() && !isValidating, - shape = RoundedCornerShape(12.dp), - ) { - if (isValidating) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - ) - Spacer(Modifier.width(8.dp)) - Text(stringResource(Res.string.validating_repo)) + label = if (isValidating) { + stringResource(Res.string.validating_repo) } else { - Text( - text = stringResource(Res.string.link_and_track), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - ) - } - } + stringResource(Res.string.link_and_track) + }, + variant = GhsButtonVariant.Tonal, + enabled = repoUrl.isNotBlank() && !isValidating, + loading = isValidating, + modifier = Modifier.fillMaxWidth(), + ) if (isValidating && validationStatus != null) { Spacer(Modifier.height(8.dp)) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt index 858467ea7..a33d30a67 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Update -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.LinearWavyProgressIndicator @@ -39,6 +38,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.apps.presentation.model.UpdateAllProgress +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_hide import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_show @@ -144,28 +145,15 @@ fun UpdatesBanner( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - Button( + GhsButton( onClick = onUpdateAll, + label = stringResource(Res.string.update_all), + variant = GhsButtonVariant.Primary, enabled = updateAllEnabled, - modifier = Modifier.weight(1f).height(44.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text( - text = stringResource(Res.string.update_all), - fontWeight = FontWeight.SemiBold, - ) - } + leadingIcon = Icons.Default.Update, + modifier = Modifier.weight(1f), + ) + // TODO(ghs-button): needs onPrimaryContainer border/ink to match banner context OutlinedButton( onClick = onToggleExpanded, modifier = Modifier.height(44.dp), @@ -223,6 +211,7 @@ private fun UpdateAllInlineProgress( overflow = TextOverflow.Ellipsis, ) } + // TODO(ghs-button): needs onPrimaryContainer border/ink to match banner context OutlinedButton( onClick = onCancel, modifier = Modifier.height(38.dp), diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt index 131bd5dcc..7f6c99075 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/VariantPickerDialog.kt @@ -28,7 +28,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -145,17 +147,21 @@ fun VariantPickerDialog( }, confirmButton = { - TextButton( + GhsButton( onClick = { onAction(AppsAction.OnDismissVariantPicker) onAction(AppsAction.OnOpenAdvancedSettings(app)) }, - ) { - Text(stringResource(Res.string.variant_picker_open_filter)) - } - TextButton(onClick = { onAction(AppsAction.OnDismissVariantPicker) }) { - Text(stringResource(Res.string.cancel)) - } + label = stringResource(Res.string.variant_picker_open_filter), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) + GhsButton( + onClick = { onAction(AppsAction.OnDismissVariantPicker) }, + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/AutoImportSummaryScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/AutoImportSummaryScreen.kt index bbf632bb9..2dddb5b86 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/AutoImportSummaryScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/AutoImportSummaryScreen.kt @@ -12,10 +12,8 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,6 +25,8 @@ import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.external_import_auto_summary_body import zed.rainxch.githubstore.core.presentation.res.external_import_auto_summary_continue @@ -97,12 +97,16 @@ fun AutoImportSummaryScreen( } Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton(onClick = onUndoAll) { - Text(stringResource(Res.string.external_import_auto_summary_undo_all)) - } - Button(onClick = onContinue) { - Text(stringResource(Res.string.external_import_auto_summary_continue)) - } + GhsButton( + onClick = onUndoAll, + label = stringResource(Res.string.external_import_auto_summary_undo_all), + variant = GhsButtonVariant.Outline, + ) + GhsButton( + onClick = onContinue, + label = stringResource(Res.string.external_import_auto_summary_continue), + variant = GhsButtonVariant.Primary, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt index 5af5e478a..6febbe559 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt @@ -16,13 +16,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -133,19 +132,18 @@ fun CandidateCard( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { - OutlinedButton( + GhsButton( onClick = onSkip, + label = stringResource(Res.string.external_import_card_action_skip), + variant = GhsButtonVariant.Outline, modifier = Modifier.weight(1f), - ) { - Text(stringResource(Res.string.external_import_card_action_skip)) - } - TextButton(onClick = onToggleExpanded) { - Text(stringResource(Res.string.external_import_card_action_less)) - Icon( - imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = null, - ) - } + ) + GhsButton( + onClick = onToggleExpanded, + label = stringResource(Res.string.external_import_card_action_less), + variant = GhsButtonVariant.Text, + trailingIcon = Icons.Default.KeyboardArrowUp, + ) } } } @@ -165,23 +163,20 @@ private fun CollapsedActions( verticalAlignment = Alignment.CenterVertically, ) { if (canLink) { - Button( + GhsButton( onClick = onLink, + label = stringResource(Res.string.external_import_card_action_link), + variant = GhsButtonVariant.Primary, modifier = Modifier.weight(1f), - ) { - Text(stringResource(Res.string.external_import_card_action_link)) - } + ) } - TextButton( + GhsButton( onClick = onExpand, + label = stringResource(Res.string.external_import_card_action_more), + variant = GhsButtonVariant.Text, + trailingIcon = Icons.Default.KeyboardArrowDown, modifier = if (canLink) Modifier else Modifier.weight(1f), - ) { - Text(stringResource(Res.string.external_import_card_action_more)) - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - ) - } + ) } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CompletionToast.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CompletionToast.kt index c9c19a521..a63d125e7 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CompletionToast.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CompletionToast.kt @@ -8,11 +8,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -73,9 +74,11 @@ fun CompletionToast( ) } - Button(onClick = onExit) { - Text(stringResource(Res.string.external_import_completion_action_view_all)) - } + GhsButton( + onClick = onExit, + label = stringResource(Res.string.external_import_completion_action_view_all), + variant = GhsButtonVariant.Primary, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/EmptyStateScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/EmptyStateScreen.kt index 1697fa674..02e4da917 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/EmptyStateScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/EmptyStateScreen.kt @@ -10,13 +10,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Search -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -62,12 +61,16 @@ fun EmptyStateScreen( color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, ) - Button(onClick = onExit) { - Text(stringResource(Res.string.external_import_empty_done)) - } - TextButton(onClick = onAddManually) { - Text(stringResource(Res.string.external_import_empty_add_manually)) - } + GhsButton( + onClick = onExit, + label = stringResource(Res.string.external_import_empty_done), + variant = GhsButtonVariant.Primary, + ) + GhsButton( + onClick = onAddManually, + label = stringResource(Res.string.external_import_empty_add_manually), + variant = GhsButtonVariant.Text, + ) } else { Icon( imageVector = Icons.Outlined.Search, @@ -89,16 +92,22 @@ fun EmptyStateScreen( textAlign = TextAlign.Center, ) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton(onClick = onExit) { - Text(stringResource(Res.string.external_import_empty_ok)) - } - Button(onClick = onRequestPermission) { - Text(stringResource(Res.string.external_import_empty_grant_permission)) - } - } - TextButton(onClick = onAddManually) { - Text(stringResource(Res.string.external_import_empty_add_manually)) + GhsButton( + onClick = onExit, + label = stringResource(Res.string.external_import_empty_ok), + variant = GhsButtonVariant.Outline, + ) + GhsButton( + onClick = onRequestPermission, + label = stringResource(Res.string.external_import_empty_grant_permission), + variant = GhsButtonVariant.Primary, + ) } + GhsButton( + onClick = onAddManually, + label = stringResource(Res.string.external_import_empty_add_manually), + variant = GhsButtonVariant.Text, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt index 4f3379fde..69727a6b0 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProgressScreen.kt @@ -12,7 +12,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -87,12 +88,11 @@ fun ImportProgressScreen( enter = fadeIn(), exit = fadeOut(), ) { - TextButton(onClick = onSkip) { - Text( - text = stringResource(Res.string.external_import_progress_skip), - style = MaterialTheme.typography.labelLarge, - ) - } + GhsButton( + onClick = onSkip, + label = stringResource(Res.string.external_import_progress_skip), + variant = GhsButtonVariant.Text, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProposalBanner.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProposalBanner.kt index 4fe82b649..ec6162b72 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProposalBanner.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/ImportProposalBanner.kt @@ -16,7 +16,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -79,11 +81,12 @@ fun ImportProposalBanner( Spacer(Modifier.width(8.dp)) - TextButton(onClick = onReview) { - Text( - text = stringResource(Res.string.external_import_proposal_banner_review), - ) - } + GhsButton( + onClick = onReview, + label = stringResource(Res.string.external_import_proposal_banner_review), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) IconButton(onClick = onDismiss) { Icon( diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt index cbefc8220..d7835b268 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/PermissionRationaleScreen.kt @@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -72,25 +72,27 @@ fun PermissionRationaleScreen( ) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton( + GhsButton( onClick = { onAction(ExternalImportAction.OnPermissionDenied(sdkInt)) }, - ) { - Text(stringResource(Res.string.external_import_permission_not_now)) - } - Button(onClick = { - scope.launch { - onAction(ExternalImportAction.OnRequestPermission) + label = stringResource(Res.string.external_import_permission_not_now), + variant = GhsButtonVariant.Outline, + ) + GhsButton( + onClick = { + scope.launch { + onAction(ExternalImportAction.OnRequestPermission) - val action = if (requester.isGranted()) { - ExternalImportAction.OnPermissionGranted(sdkInt) - } else { - ExternalImportAction.OnPermissionDenied(sdkInt) + val action = if (requester.isGranted()) { + ExternalImportAction.OnPermissionGranted(sdkInt) + } else { + ExternalImportAction.OnPermissionDenied(sdkInt) + } + onAction(action) } - onAction(action) - } - }) { - Text(stringResource(Res.string.external_import_permission_continue)) - } + }, + label = stringResource(Res.string.external_import_permission_continue), + variant = GhsButtonVariant.Primary, + ) } } } diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/WizardList.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/WizardList.kt index 3ceae3d12..3be23f352 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/WizardList.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/WizardList.kt @@ -15,7 +15,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.LiveRegionMode @@ -91,22 +92,13 @@ fun WizardList( @Composable private fun AddManuallyFooter(onClick: () -> Unit) { - TextButton( + GhsButton( onClick = onClick, + label = stringResource(Res.string.external_import_list_add_manually), + variant = GhsButtonVariant.Text, + trailingIcon = Icons.AutoMirrored.Filled.ArrowForward, modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(Res.string.external_import_list_add_manually), - style = MaterialTheme.typography.bodyMedium, - ) - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - modifier = Modifier - .padding(start = 8.dp) - .size(16.dp), - ) - } + ) } @Composable diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt index 40b2a9e58..25aa05699 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt @@ -18,8 +18,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Search import androidx.compose.material3.AssistChip -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip @@ -41,6 +39,9 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.apps.presentation.starred.components.StarredCandidateRow +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res @@ -267,13 +268,12 @@ private fun RateLimitedBanner(onResume: () -> Unit) { color = MaterialTheme.colorScheme.error, modifier = Modifier.weight(1f), ) - Button( + GhsButton( onClick = onResume, - colors = ButtonDefaults.buttonColors(), - shape = RoundedCornerShape(12.dp), - ) { - Text(stringResource(Res.string.starred_picker_resume)) - } + label = stringResource(Res.string.starred_picker_resume), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) } } diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 9726bd8d9..8b6f17477 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -57,7 +57,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -86,6 +85,9 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.auth.presentation.model.AuthLoginState import zed.rainxch.auth.presentation.model.GithubDeviceStartUi +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation @@ -272,49 +274,36 @@ private fun StateLoggedOut(onAction: (AuthenticationAction) -> Unit) { Spacer(Modifier.height(10.dp)) - TextButton(onClick = { onAction(AuthenticationAction.SkipLogin) }) { - Text( - text = stringResource(Res.string.continue_as_guest), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + GhsButton( + onClick = { onAction(AuthenticationAction.SkipLogin) }, + label = stringResource(Res.string.continue_as_guest), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) - TextButton(onClick = { showMoreOptions = !showMoreOptions }) { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - Text( - text = if (showMoreOptions) "Hide options" else "More sign-in options", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = null, - modifier = Modifier - .size(16.dp) - .graphicsLayer { rotationZ = if (showMoreOptions) 180f else 0f }, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } + GhsButton( + onClick = { showMoreOptions = !showMoreOptions }, + label = if (showMoreOptions) "Hide options" else "More sign-in options", + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + trailingIcon = Icons.Default.ExpandMore, + ) AnimatedVisibility(visible = showMoreOptions) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Spacer(Modifier.height(4.dp)) - TextButton(onClick = { onAction(AuthenticationAction.OpenPatSheet) }) { - Text( - text = stringResource(Res.string.pat_use_token_instead), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - ) - } - TextButton(onClick = { onAction(AuthenticationAction.StartLogin) }) { - Text( - text = stringResource(Res.string.auth_use_device_code_instead), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + GhsButton( + onClick = { onAction(AuthenticationAction.OpenPatSheet) }, + label = stringResource(Res.string.pat_use_token_instead), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) + GhsButton( + onClick = { onAction(AuthenticationAction.StartLogin) }, + label = stringResource(Res.string.auth_use_device_code_instead), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } @@ -669,17 +658,17 @@ private fun StateError( onClick = { onAction(AuthenticationAction.StartLogin) }, ) Spacer(Modifier.height(6.dp)) - TextButton(onClick = { onAction(AuthenticationAction.SkipLogin) }) { - Text( - text = stringResource(Res.string.continue_as_guest), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + GhsButton( + onClick = { onAction(AuthenticationAction.SkipLogin) }, + label = stringResource(Res.string.continue_as_guest), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) Spacer(Modifier.weight(2f)) } } +// TODO(ghs-button): PrimaryPillButton wraps Button with composite Row(icon + text) content. @Composable private fun PrimaryPillButton( text: String, @@ -855,6 +844,7 @@ private fun PatSignInSheet( } } + // TODO(ghs-button): composite content (CircularProgressIndicator + Text), needs custom width/height match Button( onClick = { onAction(AuthenticationAction.SubmitPat) }, enabled = !isSubmitting && input.isNotBlank(), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index b7ffbb8c9..f5ce3aa7f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -52,7 +52,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshBox @@ -93,6 +92,9 @@ import zed.rainxch.core.domain.model.InstallSource import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.presentation.components.FloatingPill import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.locals.LocalContentWidth import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme @@ -284,28 +286,25 @@ fun DetailsRoot( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnDismissDowngradeWarning) viewModel.onAction(DetailsAction.UninstallApp) }, - ) { - Text( - text = stringResource(Res.string.uninstall_first), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.uninstall_first), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnDismissDowngradeWarning) }, - ) { - Text( - text = stringResource(Res.string.cancel), - ) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -335,27 +334,24 @@ fun DetailsRoot( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnOverrideSigningKeyWarning) }, - ) { - Text( - text = stringResource(Res.string.install_anyway), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.install_anyway), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnDismissSigningKeyWarning) }, - ) { - Text( - text = stringResource(Res.string.cancel), - ) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -381,25 +377,24 @@ fun DetailsRoot( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnConfirmUninstall) }, - ) { - Text( - text = stringResource(Res.string.uninstall), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.uninstall), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnDismissUninstallConfirmation) }, - ) { - Text(text = stringResource(Res.string.cancel)) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -425,25 +420,24 @@ fun DetailsRoot( ) }, confirmButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnConfirmUnlinkExternalApp) }, - ) { - Text( - text = stringResource(Res.string.details_unlink_external_app_dialog_confirm), - color = MaterialTheme.colorScheme.error, - ) - } + label = stringResource(Res.string.details_unlink_external_app_dialog_confirm), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OnDismissUnlinkConfirmation) }, - ) { - Text(text = stringResource(Res.string.cancel)) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -466,22 +460,24 @@ fun DetailsRoot( Text(text = stringResource(Res.string.install_permission_blocked_message)) }, confirmButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.OpenWithExternalInstaller) }, - ) { - Text(text = stringResource(Res.string.open_with_external_installer)) - } + label = stringResource(Res.string.open_with_external_installer), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton( + GhsButton( onClick = { viewModel.onAction(DetailsAction.DismissExternalInstallerPrompt) }, - ) { - Text(text = stringResource(Res.string.dismiss)) - } + label = stringResource(Res.string.dismiss), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt index 5f793ac64..d4fe6c305 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt @@ -164,6 +164,7 @@ private fun Coachmark(onDismiss: () -> Unit) { modifier = Modifier.padding(top = 2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { + // TODO(ghs-button): needs onPrimary text color in coachmark popup TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.apk_inspect_coachmark_dismiss), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt index 068ac5501..6b0dbb89e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LinkedRepoBanner.kt @@ -18,13 +18,15 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.details_linked_repo_banner_body import zed.rainxch.githubstore.core.presentation.res.details_linked_repo_banner_title @@ -75,15 +77,13 @@ fun LinkedRepoBanner( Spacer(Modifier.width(8.dp)) - TextButton(onClick = onUnlink) { - Icon( - imageVector = Icons.Default.LinkOff, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Spacer(Modifier.width(4.dp)) - Text(text = stringResource(Res.string.details_unlink_external_app_dialog_confirm)) - } + GhsButton( + onClick = onUnlink, + label = stringResource(Res.string.details_unlink_external_app_dialog_confirm), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + leadingIcon = Icons.Default.LinkOff, + ) } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt index afa9b19eb..d7ea951ab 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ReleaseAssetsPicker.kt @@ -58,6 +58,9 @@ import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.GithubAsset import zed.rainxch.core.domain.model.GithubUser import zed.rainxch.core.domain.util.AssetVariant +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.theme.tokens.Radii @@ -219,9 +222,12 @@ private fun ReleaseAssetsItemsPicker( color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), ) - androidx.compose.material3.TextButton(onClick = onUnpin) { - Text(stringResource(Res.string.variant_picker_unpin)) - } + GhsButton( + onClick = onUnpin, + label = stringResource(Res.string.variant_picker_unpin), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt index 816f40c63..cb9b3032f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationCard.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,12 +19,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.outlined.Language import androidx.compose.material.icons.outlined.Translate -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -37,6 +32,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.details.presentation.model.SupportedLanguages import zed.rainxch.details.presentation.model.TranslationState import zed.rainxch.githubstore.core.presentation.res.Res @@ -230,72 +227,39 @@ private fun ActionRow( ) { when { state.isTranslating -> { - OutlinedButton( + GhsButton( onClick = onCancel, - modifier = Modifier - .fillMaxWidth() - .height(46.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - ) - Spacer(Modifier.width(10.dp)) - Text(stringResource(Res.string.translation_card_cancel), fontWeight = FontWeight.SemiBold) - } + label = stringResource(Res.string.translation_card_cancel), + variant = GhsButtonVariant.Outline, + loading = true, + modifier = Modifier.fillMaxWidth(), + ) } state.translatedText != null -> { - OutlinedButton( + GhsButton( onClick = onToggle, - modifier = Modifier - .fillMaxWidth() - .height(46.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - Text( - text = if (state.isShowingTranslation) { - stringResource(Res.string.translation_card_show_original) - } else { - stringResource(Res.string.translation_card_show_translation) - }, - fontWeight = FontWeight.SemiBold, - ) - } + label = if (state.isShowingTranslation) { + stringResource(Res.string.translation_card_show_original) + } else { + stringResource(Res.string.translation_card_show_translation) + }, + variant = GhsButtonVariant.Outline, + modifier = Modifier.fillMaxWidth(), + ) } else -> { - Button( + GhsButton( onClick = { onTranslate(effectiveTargetCode) }, - modifier = Modifier - .fillMaxWidth() - .height(46.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, + label = stringResource( + Res.string.translation_card_translate_to, + effectiveTargetName, ), - ) { - Icon( - imageVector = Icons.Outlined.Translate, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text( - text = stringResource( - Res.string.translation_card_translate_to, - effectiveTargetName, - ), - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + variant = GhsButtonVariant.Primary, + leadingIcon = Icons.Outlined.Translate, + modifier = Modifier.fillMaxWidth(), + ) } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt index 771363f75..51122de71 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt @@ -327,6 +327,7 @@ private fun ChannelChipCoachmark(onDismiss: () -> Unit) { modifier = Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = Arrangement.End, ) { + // TODO(ghs-button): needs onPrimary text color in coachmark popup, not standard primary TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.channel_chip_coachmark_dismiss), diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/states/ErrorState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/states/ErrorState.kt index bc7746e8a..394a3ce97 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/states/ErrorState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/states/ErrorState.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -20,12 +19,9 @@ import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material.icons.outlined.HourglassEmpty import androidx.compose.material.icons.outlined.SearchOff import androidx.compose.material.icons.outlined.SentimentDissatisfied -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,6 +34,8 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.error_state_go_back @@ -109,28 +107,18 @@ fun ErrorState( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - TextButton( + GhsButton( onClick = { onAction(DetailsAction.OnNavigateBackClick) }, + label = stringResource(Res.string.error_state_go_back), + variant = GhsButtonVariant.Text, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp), - ) { - Text( - text = stringResource(Res.string.error_state_go_back), - fontWeight = FontWeight.SemiBold, - ) - } - Button( + ) + GhsButton( onClick = { onAction(DetailsAction.Retry) }, + label = stringResource(Res.string.retry), + variant = GhsButtonVariant.Primary, modifier = Modifier.weight(1f), - shape = RoundedCornerShape(16.dp), - contentPadding = PaddingValues(vertical = 12.dp), - colors = ButtonDefaults.buttonColors(), - ) { - Text( - text = stringResource(Res.string.retry), - fontWeight = FontWeight.SemiBold, - ) - } + ) } } } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt index 97c7bac8b..aa2e01b20 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt @@ -29,7 +29,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -211,15 +213,14 @@ fun DeveloperProfileScreen( .align(Alignment.BottomCenter) .padding(16.dp), action = { - TextButton( + GhsButton( onClick = { onAction(DeveloperProfileAction.OnRetry) }, - ) { - Text( - text = stringResource(Res.string.retry), - ) - } + label = stringResource(Res.string.retry), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissAction = { IconButton( diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 4f114c518..3cb29ba73 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -33,7 +33,8 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.ScrollbarContainer -import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.section.SectionHeader import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled @@ -154,9 +155,11 @@ private fun HomeScreen( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) - OutlineButton(onClick = { onAction(HomeAction.OnRetry) }) { - Text(text = stringResource(Res.string.home_retry)) - } + GhsButton( + onClick = { onAction(HomeAction.OnRetry) }, + label = stringResource(Res.string.home_retry), + variant = GhsButtonVariant.Outline, + ) } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt index 182c237a4..7e017d43f 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/StarredRowItem.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -30,7 +29,8 @@ import zed.rainxch.githubstore.core.presentation.res.home_action_get import zed.rainxch.githubstore.core.presentation.res.open import zed.rainxch.githubstore.core.presentation.res.update import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.buttons.OutlineButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.cards.RowCard import zed.rainxch.core.presentation.vocabulary.PlatformGlyph import zed.rainxch.core.presentation.vocabulary.StarTier @@ -97,17 +97,17 @@ fun StarredRowItem( } } } - OutlineButton(onClick = onClick) { - Text( - text = stringResource( - when { - card.isUpdateAvailable -> Res.string.update - card.isInstalled -> Res.string.open - else -> Res.string.home_action_get - }, - ), - ) - } + GhsButton( + onClick = onClick, + label = stringResource( + when { + card.isUpdateAvailable -> Res.string.update + card.isInstalled -> Res.string.open + else -> Res.string.home_action_get + }, + ), + variant = GhsButtonVariant.Outline, + ) } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt index ac8c1ce1a..32005d13a 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/TrendingRowItem.kt @@ -3,7 +3,6 @@ package zed.rainxch.home.presentation.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -15,18 +14,17 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.color.avatarColorFor import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.buttons.PrimaryButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.cards.RepoStripeCard import zed.rainxch.core.presentation.components.chips.PlatformsChip import zed.rainxch.core.presentation.components.chips.StatChip @@ -161,31 +159,18 @@ private fun RankRowItem( }, languagePill = null, cta = { - PrimaryButton( + GhsButton( onClick = onClick, - backgroundColor = MaterialTheme.colorScheme.onSurface, - contentColor = MaterialTheme.colorScheme.surface, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource( - when { - card.isUpdateAvailable -> Res.string.update - card.isInstalled -> Res.string.open - else -> Res.string.home_action_get - }, - ), - ) - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - } - } + label = stringResource( + when { + card.isUpdateAvailable -> Res.string.update + card.isInstalled -> Res.string.open + else -> Res.string.home_action_get + }, + ), + variant = GhsButtonVariant.Primary, + trailingIcon = Icons.AutoMirrored.Filled.ArrowForward, + ) }, ) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt index 827c0602a..2dbf9ce24 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt @@ -8,13 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -72,32 +72,19 @@ fun LogoutDialog( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), ) { - TextButton( - onClick = { - onDismissRequest() - }, - ) { - Text( - text = stringResource(Res.string.close), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + GhsButton( + onClick = { onDismissRequest() }, + label = stringResource(Res.string.close), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) - Button( + GhsButton( onClick = onLogout, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Text( - text = stringResource(Res.string.logout), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + label = stringResource(Res.string.logout), + variant = GhsButtonVariant.Destructive, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt index 4528d8d55..85bb083f6 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -19,8 +18,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -38,6 +35,9 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.UserProfile import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.* @@ -158,26 +158,15 @@ fun LazyListScope.accountSection( textAlign = TextAlign.Center, ) Spacer(Modifier.height(16.dp)) - Button( + GhsButton( onClick = { onAction(ProfileAction.OnLoginClick) }, + label = stringResource(Res.string.profile_login), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Lg, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp) - .height(50.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 18.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text( - text = stringResource(Res.string.profile_login), - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } + .padding(horizontal = 8.dp), + ) } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 3139b65f3..a6dfde416 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -51,7 +51,6 @@ import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilterChip -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -90,6 +89,8 @@ import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.components.RepositoryCard import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme @@ -1007,26 +1008,22 @@ private fun ExploreFromGithubButton( ) { when (status) { SearchState.ExploreStatus.IDLE -> { - OutlinedButton(onClick = onExplore) { - Icon( - imageVector = Icons.Outlined.TravelExplore, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text(text = stringResource(Res.string.fetch_more_from_github)) - } + GhsButton( + onClick = onExplore, + label = stringResource(Res.string.fetch_more_from_github), + variant = GhsButtonVariant.Outline, + leadingIcon = Icons.Outlined.TravelExplore, + ) } SearchState.ExploreStatus.LOADING -> { - OutlinedButton(onClick = {}, enabled = false) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - ) - Spacer(Modifier.width(8.dp)) - Text(text = stringResource(Res.string.fetching_from_github)) - } + GhsButton( + onClick = {}, + label = stringResource(Res.string.fetching_from_github), + variant = GhsButtonVariant.Outline, + enabled = false, + loading = true, + ) } SearchState.ExploreStatus.EXHAUSTED -> { diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt index 9d0f12eeb..b1010625b 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchFiltersSheet.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -25,12 +24,9 @@ import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Language -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -41,6 +37,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.collections.immutable.ImmutableList import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.search_filters_apply @@ -95,15 +94,12 @@ fun SearchFiltersSheet( color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), ) - TextButton(onClick = onReset) { - Text( - text = stringResource(Res.string.search_filters_reset), - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.primary, - ) - } + GhsButton( + onClick = onReset, + label = stringResource(Res.string.search_filters_reset), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } FilterSection(title = stringResource(Res.string.search_filters_section_source)) { @@ -156,25 +152,13 @@ fun SearchFiltersSheet( Spacer(Modifier.height(4.dp)) - Button( + GhsButton( onClick = onDismiss, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text( - text = stringResource(Res.string.search_filters_apply), - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } + label = stringResource(Res.string.search_filters_apply), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Lg, + modifier = Modifier.fillMaxWidth(), + ) Spacer(Modifier.height(8.dp)) } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchHistorySection.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchHistorySection.kt index 6f4c88ecc..12fc029f3 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchHistorySection.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SearchHistorySection.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,6 +24,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.* @Composable @@ -48,12 +50,12 @@ fun SearchHistorySection( fontWeight = FontWeight.Medium, ) - TextButton(onClick = onClearAll) { - Text( - text = stringResource(Res.string.clear_all_history), - style = MaterialTheme.typography.labelMedium, - ) - } + GhsButton( + onClick = onClearAll, + label = stringResource(Res.string.clear_all_history), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } recentSearches.forEach { query -> diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt index 49c3189b3..fc4678f0f 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt @@ -13,6 +13,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -37,9 +40,12 @@ fun SortByBottomSheet( onDismissRequest = onDismissRequest, confirmButton = {}, dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(Res.string.close)) - } + GhsButton( + onClick = onDismissRequest, + label = stringResource(Res.string.close), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, title = { Text( @@ -54,6 +60,7 @@ fun SortByBottomSheet( ) { SortByUi.entries.forEach { option -> val isSelected = option == selectedSortBy + // TODO(ghs-button): needs per-item color (primary when selected, onSurface otherwise) TextButton( onClick = { onSortBySelected(option) diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt index 26dc244d8..62f27e30c 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt @@ -37,7 +37,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -60,6 +59,9 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.GithubStoreButton import zed.rainxch.core.presentation.components.ScrollbarContainer +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme @@ -239,15 +241,14 @@ fun StarredScreen( .align(Alignment.BottomCenter) .padding(16.dp), action = { - TextButton( + GhsButton( onClick = { onAction(StarredReposAction.OnRetrySync) }, - ) { - Text( - text = stringResource(Res.string.retry), - ) - } + label = stringResource(Res.string.retry), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissAction = { IconButton( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt index c14b150d5..34fcf9e2d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/ClearDownloadsDialog.kt @@ -8,13 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -67,30 +67,19 @@ fun ClearDownloadsDialog( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), ) { - TextButton( + GhsButton( onClick = onDismissRequest, - ) { - Text( - text = stringResource(Res.string.cancel), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) - Button( + GhsButton( onClick = onConfirm, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Text( - text = stringResource(Res.string.delete_all), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer, - ) - } + label = stringResource(Res.string.delete_all), + variant = GhsButtonVariant.Destructive, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt index d455e9fb8..1ee969c14 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/CustomForgesDialog.kt @@ -18,11 +18,13 @@ import androidx.compose.material3.InputChip import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction @@ -70,9 +72,12 @@ fun CustomForgesDialog( isError = state.customForgeError != null, modifier = Modifier.weight(1f), ) - TextButton(onClick = { onAction(TweaksAction.OnAddCustomForge) }) { - Text(stringResource(Res.string.custom_forges_add_button)) - } + GhsButton( + onClick = { onAction(TweaksAction.OnAddCustomForge) }, + label = stringResource(Res.string.custom_forges_add_button), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } if (state.customForgeError != null) { Text( @@ -112,9 +117,12 @@ fun CustomForgesDialog( } }, confirmButton = { - TextButton(onClick = { onAction(TweaksAction.OnDismissCustomForgesDialog) }) { - Text(stringResource(Res.string.done)) - } + GhsButton( + onClick = { onAction(TweaksAction.OnDismissCustomForgesDialog) }, + label = stringResource(Res.string.done), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt index 5090788fc..a25fd0732 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt @@ -8,12 +8,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,7 +18,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.RestartReason -import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.restart_banner_body @@ -86,23 +85,19 @@ fun RestartBanner( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - TextButton(onClick = onLater) { - Text( - text = stringResource(Res.string.restart_banner_later), - color = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } + GhsButton( + onClick = onLater, + label = stringResource(Res.string.restart_banner_later), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) Spacer(Modifier.height(0.dp)) - FilledTonalButton( + GhsButton( onClick = onRestartNow, - shape = WonkySquircleShape.CtaPrimary, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text(text = stringResource(Res.string.restart_banner_restart_now)) - } + label = stringResource(Res.string.restart_banner_restart_now), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index 1166c1212..29e57f5b1 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -28,7 +28,6 @@ import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Speed import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -55,6 +54,9 @@ import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.model.RootAvailability import zed.rainxch.core.domain.model.ShizukuAvailability import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction @@ -226,16 +228,12 @@ private fun CustomInstallerEditor( isError = state.installerAttributionCustomError != null, supportingText = customSupporting, ) - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnInstallerAttributionCustomSave) }, + label = stringResource(Res.string.installer_attribution_custom_apply), + variant = GhsButtonVariant.Tonal, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - ) { - Text( - text = stringResource(Res.string.installer_attribution_custom_apply), - fontWeight = FontWeight.SemiBold, - ) - } + ) } } @@ -537,17 +535,12 @@ private fun RootStatusActions( ) { when (availability) { RootAvailability.PERMISSION_NEEDED -> { - FilledTonalButton( + GhsButton( onClick = onRequestPermission, + label = stringResource(Res.string.root_grant_permission), + variant = GhsButtonVariant.Tonal, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - ) { - Text( - text = stringResource(Res.string.root_grant_permission), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - ) - } + ) } RootAvailability.UNAVAILABLE -> { HintText(text = stringResource(Res.string.root_unavailable_hint)) @@ -584,17 +577,12 @@ private fun ShizukuStatusActions( ) { when (availability) { ShizukuAvailability.PERMISSION_NEEDED -> { - FilledTonalButton( + GhsButton( onClick = onRequestPermission, + label = stringResource(Res.string.shizuku_grant_permission), + variant = GhsButtonVariant.Tonal, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Text( - text = stringResource(Res.string.shizuku_grant_permission), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } + ) } ShizukuAvailability.UNAVAILABLE -> { HintText(text = stringResource(Res.string.shizuku_install_hint)) @@ -614,17 +602,12 @@ private fun DhizukuStatusActions( ) { when (availability) { DhizukuAvailability.PERMISSION_NEEDED -> { - FilledTonalButton( + GhsButton( onClick = onRequestPermission, + label = stringResource(Res.string.dhizuku_grant_permission), + variant = GhsButtonVariant.Tonal, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Text( - text = stringResource(Res.string.dhizuku_grant_permission), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } + ) } DhizukuAvailability.UNAVAILABLE -> { HintText(text = stringResource(Res.string.dhizuku_install_hint)) @@ -1006,12 +989,18 @@ private fun BatteryOptimizationCard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), ) { - androidx.compose.material3.TextButton(onClick = onDismiss) { - Text(stringResource(Res.string.battery_optimization_card_dismiss)) - } - FilledTonalButton(onClick = onOpenSettings) { - Text(stringResource(Res.string.battery_optimization_card_open)) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.battery_optimization_card_dismiss), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) + GhsButton( + onClick = onOpenSettings, + label = stringResource(Res.string.battery_optimization_card_open), + variant = GhsButtonVariant.Tonal, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt index 28e5f0683..616edb72c 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -50,6 +49,9 @@ import androidx.compose.foundation.text.KeyboardOptions import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.SupportedTranslationLanguages import zed.rainxch.core.domain.model.TranslationProvider +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformation @@ -402,18 +404,13 @@ private fun YoudaoCredentialsForm( modifier = Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically, ) { - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnYoudaoCredentialsSave) }, + label = stringResource(Res.string.translation_youdao_save), + variant = GhsButtonVariant.Tonal, enabled = canSave, - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.translation_youdao_save)) - } + leadingIcon = Icons.Default.Save, + ) } } } @@ -466,18 +463,13 @@ private fun LibreTranslateCredentialsForm( modifier = Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically, ) { - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnLibreTranslateCredentialsSave) }, + label = stringResource(Res.string.translation_libre_save), + variant = GhsButtonVariant.Tonal, enabled = canSave, - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.translation_libre_save)) - } + leadingIcon = Icons.Default.Save, + ) } } } @@ -500,12 +492,13 @@ private fun DeeplCredentialsForm( color = MaterialTheme.colorScheme.onSurfaceVariant, ) - androidx.compose.material3.TextButton( + GhsButton( onClick = { runCatching { uriHandler.openUri("https://www.deepl.com/pro-api") } }, + label = stringResource(Res.string.translation_deepl_get_free_key), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, modifier = Modifier.align(Alignment.Start), - ) { - Text(stringResource(Res.string.translation_deepl_get_free_key)) - } + ) GhsTextField( value = state.deeplAuthKey, @@ -527,18 +520,13 @@ private fun DeeplCredentialsForm( modifier = Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically, ) { - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnDeeplCredentialsSave) }, + label = stringResource(Res.string.translation_deepl_save), + variant = GhsButtonVariant.Tonal, enabled = canSave, - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.translation_deepl_save)) - } + leadingIcon = Icons.Default.Save, + ) } } } @@ -561,12 +549,13 @@ private fun MicrosoftCredentialsForm( color = MaterialTheme.colorScheme.onSurfaceVariant, ) - androidx.compose.material3.TextButton( + GhsButton( onClick = { runCatching { uriHandler.openUri("https://portal.azure.com/#create/Microsoft.CognitiveServicesTextTranslation") } }, + label = stringResource(Res.string.translation_microsoft_get_free_key), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, modifier = Modifier.align(Alignment.Start), - ) { - Text(stringResource(Res.string.translation_microsoft_get_free_key)) - } + ) GhsTextField( value = state.microsoftTranslatorKey, @@ -597,18 +586,13 @@ private fun MicrosoftCredentialsForm( modifier = Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically, ) { - FilledTonalButton( + GhsButton( onClick = { onAction(TweaksAction.OnMicrosoftTranslatorCredentialsSave) }, + label = stringResource(Res.string.translation_microsoft_save), + variant = GhsButtonVariant.Tonal, enabled = canSave, - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(8.dp)) - Text(stringResource(Res.string.translation_microsoft_save)) - } + leadingIcon = Icons.Default.Save, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/SendActions.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/SendActions.kt index 3cd9aeb33..8eb1ecdd1 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/SendActions.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/SendActions.kt @@ -3,16 +3,12 @@ package zed.rainxch.tweaks.presentation.feedback.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.feedback_send_via_email import zed.rainxch.githubstore.core.presentation.res.feedback_send_via_github @@ -29,35 +25,21 @@ fun SendActions( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedButton( + GhsButton( onClick = onSendGithub, + label = stringResource(Res.string.feedback_send_via_github), + variant = GhsButtonVariant.Outline, enabled = canSend, + loading = isSending, modifier = Modifier.weight(1f), - ) { - if (isSending) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.primary, - ) - } else { - Text(stringResource(Res.string.feedback_send_via_github)) - } - } - Button( + ) + GhsButton( onClick = onSendEmail, + label = stringResource(Res.string.feedback_send_via_email), + variant = GhsButtonVariant.Primary, enabled = canSend, + loading = isSending, modifier = Modifier.weight(1f), - ) { - if (isSending) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary, - ) - } else { - Text(stringResource(Res.string.feedback_send_via_email)) - } - } + ) } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt index edbbf0f69..5963338a9 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt @@ -27,7 +27,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -120,13 +122,14 @@ fun HiddenRepositoriesRoot( }, actions = { if (state.items.isNotEmpty()) { - TextButton( + GhsButton( onClick = { viewModel.onAction(HiddenRepositoriesAction.OnUnhideAll) }, - ) { - Text(stringResource(Res.string.hidden_repositories_unhide_all)) - } + label = stringResource(Res.string.hidden_repositories_unhide_all), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } }, ) @@ -230,9 +233,12 @@ private fun HiddenRepoRow( ) } - TextButton(onClick = onUnhide) { - Text(stringResource(Res.string.hidden_repositories_unhide_action)) - } + GhsButton( + onClick = onUnhide, + label = stringResource(Res.string.hidden_repositories_unhide_action), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt index 6b3cfe854..67827f1d6 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -62,6 +61,9 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.ForgeKind import zed.rainxch.core.domain.model.HostToken +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res @@ -358,18 +360,19 @@ private fun PresetForgeCard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { - TextButton(onClick = onOpenTokenCreationPage) { - Icon( - Icons.Default.OpenInBrowser, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - Spacer(Modifier.width(4.dp)) - Text(stringResource(Res.string.host_tokens_picker_open_page)) - } - TextButton(onClick = onPick) { - Text(stringResource(Res.string.host_tokens_picker_paste)) - } + GhsButton( + onClick = onOpenTokenCreationPage, + label = stringResource(Res.string.host_tokens_picker_open_page), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + leadingIcon = Icons.Default.OpenInBrowser, + ) + GhsButton( + onClick = onPick, + label = stringResource(Res.string.host_tokens_picker_paste), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } @@ -433,9 +436,12 @@ private fun PickerDialog( } }, confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(Res.string.host_tokens_action_cancel)) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.host_tokens_action_cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } @@ -613,14 +619,20 @@ private fun AddTokenDialog( } }, confirmButton = { - TextButton(onClick = { onAction(HostTokensAction.OnAddConfirm) }) { - Text(stringResource(Res.string.host_tokens_action_save)) - } + GhsButton( + onClick = { onAction(HostTokensAction.OnAddConfirm) }, + label = stringResource(Res.string.host_tokens_action_save), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, dismissButton = { - TextButton(onClick = { onAction(HostTokensAction.OnAddDismiss) }) { - Text(stringResource(Res.string.host_tokens_action_cancel)) - } + GhsButton( + onClick = { onAction(HostTokensAction.OnAddDismiss) }, + label = stringResource(Res.string.host_tokens_action_cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt index 56f0bd4ad..6da4aa6fd 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -44,6 +43,8 @@ import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.MirrorConfig import zed.rainxch.core.domain.model.MirrorPreference import zed.rainxch.core.domain.model.MirrorType +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.mirror_custom_label @@ -177,21 +178,14 @@ fun MirrorPickerRoot( } item { - Button( + GhsButton( onClick = { viewModel.onAction(MirrorPickerAction.OnTestConnection) }, + label = stringResource(Res.string.mirror_test_button), + variant = GhsButtonVariant.Primary, enabled = !state.isTesting, + loading = state.isTesting, modifier = Modifier.fillMaxWidth(), - ) { - if (state.isTesting) { - CircularProgressIndicator( - modifier = Modifier.height(18.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp, - ) - } else { - Text(stringResource(Res.string.mirror_test_button)) - } - } + ) state.testResult?.let { result -> Spacer(Modifier.height(8.dp)) Text( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt index 429145f02..47eadd919 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/components/CustomMirrorDialog.kt @@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.cancel @@ -48,17 +50,21 @@ fun CustomMirrorDialog( } }, confirmButton = { - TextButton( + GhsButton( onClick = onConfirm, + label = stringResource(Res.string.mirror_custom_save), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, enabled = draft.isNotBlank() && error == null, - ) { - Text(stringResource(Res.string.mirror_custom_save)) - } + ) }, dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(Res.string.cancel)) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) }, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt index f418ec1b7..147a932e7 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt @@ -25,7 +25,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -212,9 +214,12 @@ private fun SkippedAppRow( } } - TextButton(onClick = onUnskip) { - Text(stringResource(Res.string.skipped_updates_unskip_action)) - } + GhsButton( + onClick = onUnskip, + label = stringResource(Res.string.skipped_updates_unskip_action), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + ) } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt index 3ee05b4b4..8efdff694 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt @@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Inventory2 -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState @@ -36,6 +34,8 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents @@ -162,25 +162,13 @@ private fun DownloadsCard( ) } - FilledTonalButton( + GhsButton( onClick = onClearClick, + label = "Clear", + variant = GhsButtonVariant.Destructive, enabled = !isEmpty, - shape = WonkySquircleShape.CtaAlt, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) { - Icon( - imageVector = Icons.Outlined.DeleteOutline, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.size(4.dp)) - Text(text = "Clear") - } + leadingIcon = Icons.Outlined.DeleteOutline, + ) } } } From 9c76a9e2423016a275fe4109bf877299092b3ae9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:11:18 +0500 Subject: [PATCH 107/172] fix(ui): mute Primary GhsButton in dark mode for issue #665 --- .../core/presentation/components/buttons/GhsButton.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt index fc7f5dbe1..727893433 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -64,15 +65,18 @@ fun GhsButton( trailingIcon: ImageVector? = null, ) { val cs = MaterialTheme.colorScheme + // Dark mode mutes Primary fill — issue #665 (eye-strain on large dark monitors). + // Light: full primary. Dark: primaryContainer (low-luminance, blends into surface). + val isDark = cs.background.luminance() < 0.5f val container: Color = when (variant) { - GhsButtonVariant.Primary -> cs.primary + GhsButtonVariant.Primary -> if (isDark) cs.primaryContainer else cs.primary GhsButtonVariant.Tonal -> cs.secondaryContainer GhsButtonVariant.Outline -> Color.Transparent GhsButtonVariant.Text -> Color.Transparent GhsButtonVariant.Destructive -> cs.errorContainer } val content: Color = when (variant) { - GhsButtonVariant.Primary -> cs.onPrimary + GhsButtonVariant.Primary -> if (isDark) cs.onPrimaryContainer else cs.onPrimary GhsButtonVariant.Tonal -> cs.onSecondaryContainer GhsButtonVariant.Outline -> cs.onSurface GhsButtonVariant.Text -> cs.primary From 8b720bbe3ef015174c20cb155823c7607e310296 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:28:40 +0500 Subject: [PATCH 108/172] feat(core): GhsTextField minLines maxLines readOnly keyboardActions --- .../components/inputs/GhsTextField.kt | 9 +++++++++ .../import/components/RepoSearchOverride.kt | 17 ++++------------- .../feedback/components/ConditionalFields.kt | 11 +++++------ .../feedback/components/FeedbackBottomSheet.kt | 7 +++---- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt index 788e8e335..4622e79ca 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/inputs/GhsTextField.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility @@ -50,8 +51,12 @@ fun GhsTextField( isError: Boolean = false, enabled: Boolean = true, singleLine: Boolean = true, + minLines: Int = 1, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + readOnly: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, ) { var focused by remember { mutableStateOf(false) } val borderColor = when { @@ -127,9 +132,13 @@ fun GhsTextField( .fillMaxWidth() .onFocusChanged { focused = it.isFocused }, singleLine = singleLine, + minLines = minLines, + maxLines = maxLines, + readOnly = readOnly, enabled = enabled, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, textStyle = LocalTextStyle.current.merge( TextStyle(color = contentColor), ).copy( diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt index 73943b895..beb6958b8 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/RepoSearchOverride.kt @@ -14,9 +14,9 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.inputs.GhsTextField import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction @@ -45,23 +45,14 @@ fun RepoSearchOverride( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Box { - // TODO(ghs-text-field): needs keyboardActions support - OutlinedTextField( + GhsTextField( value = query, onValueChange = onQueryChange, modifier = Modifier.fillMaxWidth(), singleLine = true, isError = !searchError.isNullOrBlank(), - supportingText = { - if (!searchError.isNullOrBlank()) { - Text(text = searchError) - } - }, - placeholder = { - Text( - text = stringResource(Res.string.external_import_search_placeholder_url), - ) - }, + supportingText = searchError?.takeIf { it.isNotBlank() }, + placeholder = stringResource(Res.string.external_import_search_placeholder_url), trailingIcon = { IconButton(onClick = onSubmit) { Icon( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt index 3ea807181..4e204bf6a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/ConditionalFields.kt @@ -3,9 +3,8 @@ package zed.rainxch.tweaks.presentation.feedback.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import zed.rainxch.core.presentation.components.inputs.GhsTextField import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource @@ -72,18 +71,18 @@ fun ConditionalFields( } } -// TODO(ghs-text-field): needs minLines support @Composable private fun MultilineField( value: String, label: String, onValueChange: (String) -> Unit, ) { - OutlinedTextField( + GhsTextField( value = value, onValueChange = onValueChange, - label = { Text(label) }, - modifier = Modifier.fillMaxWidth(), + label = label, + singleLine = false, minLines = 3, + modifier = Modifier.fillMaxWidth(), ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt index bef5a5172..ff160f1ad 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -117,11 +116,11 @@ fun FeedbackBottomSheet( modifier = Modifier.fillMaxWidth(), ) - // TODO(ghs-text-field): needs minLines support - OutlinedTextField( + GhsTextField( value = state.description, onValueChange = { viewModel.onAction(FeedbackAction.OnDescriptionChange(it)) }, - label = { Text(stringResource(Res.string.feedback_field_description) + " *") }, + label = stringResource(Res.string.feedback_field_description) + " *", + singleLine = false, minLines = 4, modifier = Modifier.fillMaxWidth(), ) From cd8b49a7f1621d60bd7f2a939312b33e5e582384 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:34:22 +0500 Subject: [PATCH 109/172] feat(core): GhsButton content slot, clear 7 TODO sites --- .../components/buttons/GhsButton.kt | 112 +++++++++++------- .../presentation/components/UpdatesBanner.kt | 48 +++----- .../auth/presentation/AuthenticationRoot.kt | 62 +++------- .../components/InspectApkButton.kt | 21 ++-- .../components/sections/ReleaseChannel.kt | 19 +-- .../components/SortByBottomSheet.kt | 24 ++-- 6 files changed, 132 insertions(+), 154 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt index 727893433..c68cb9591 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/buttons/GhsButton.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -18,6 +19,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -55,27 +57,25 @@ enum class GhsButtonSize { @Composable fun GhsButton( onClick: () -> Unit, - label: String, modifier: Modifier = Modifier, variant: GhsButtonVariant = GhsButtonVariant.Primary, size: GhsButtonSize = GhsButtonSize.Md, enabled: Boolean = true, loading: Boolean = false, - leadingIcon: ImageVector? = null, - trailingIcon: ImageVector? = null, + containerColorOverride: Color? = null, + contentColorOverride: Color? = null, + content: @Composable RowScope.() -> Unit, ) { val cs = MaterialTheme.colorScheme - // Dark mode mutes Primary fill — issue #665 (eye-strain on large dark monitors). - // Light: full primary. Dark: primaryContainer (low-luminance, blends into surface). val isDark = cs.background.luminance() < 0.5f - val container: Color = when (variant) { + val container: Color = containerColorOverride ?: when (variant) { GhsButtonVariant.Primary -> if (isDark) cs.primaryContainer else cs.primary GhsButtonVariant.Tonal -> cs.secondaryContainer GhsButtonVariant.Outline -> Color.Transparent GhsButtonVariant.Text -> Color.Transparent GhsButtonVariant.Destructive -> cs.errorContainer } - val content: Color = when (variant) { + val contentColor: Color = contentColorOverride ?: when (variant) { GhsButtonVariant.Primary -> if (isDark) cs.onPrimaryContainer else cs.onPrimary GhsButtonVariant.Tonal -> cs.onSecondaryContainer GhsButtonVariant.Outline -> cs.onSurface @@ -113,11 +113,6 @@ fun GhsButton( GhsButtonSize.Md -> 14.sp GhsButtonSize.Lg -> 16.sp } - val iconSize = when (size) { - GhsButtonSize.Sm -> 16.dp - GhsButtonSize.Md -> 18.dp - GhsButtonSize.Lg -> 20.dp - } val interactionSource = remember { MutableInteractionSource() } val pressed by interactionSource.collectIsPressedAsState() @@ -157,40 +152,73 @@ fun GhsButton( horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, ) { - CompositionLocalProvider(LocalContentColor provides content.copy(alpha = alpha)) { - if (loading) { - CircularProgressIndicator( - modifier = Modifier.size(iconSize), - strokeWidth = 2.dp, - color = content.copy(alpha = alpha), - ) - } else if (leadingIcon != null) { - Icon( - imageVector = leadingIcon, - contentDescription = null, - modifier = Modifier.size(iconSize), - tint = content.copy(alpha = alpha), - ) - } - Text( - text = label, - style = MaterialTheme.typography.labelLarge.copy( + CompositionLocalProvider(LocalContentColor provides contentColor.copy(alpha = alpha)) { + ProvideTextStyle( + value = MaterialTheme.typography.labelLarge.copy( fontWeight = FontWeight.SemiBold, fontSize = fontSize, ), - color = content.copy(alpha = alpha), - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis, - ) - if (trailingIcon != null && !loading) { - Icon( - imageVector = trailingIcon, - contentDescription = null, - modifier = Modifier.size(iconSize), - tint = content.copy(alpha = alpha), - ) + ) { + content() } } } } + +@Composable +fun GhsButton( + onClick: () -> Unit, + label: String, + modifier: Modifier = Modifier, + variant: GhsButtonVariant = GhsButtonVariant.Primary, + size: GhsButtonSize = GhsButtonSize.Md, + enabled: Boolean = true, + loading: Boolean = false, + leadingIcon: ImageVector? = null, + trailingIcon: ImageVector? = null, + containerColorOverride: Color? = null, + contentColorOverride: Color? = null, +) { + val iconSize = when (size) { + GhsButtonSize.Sm -> 16.dp + GhsButtonSize.Md -> 18.dp + GhsButtonSize.Lg -> 20.dp + } + GhsButton( + onClick = onClick, + modifier = modifier, + variant = variant, + size = size, + enabled = enabled, + loading = loading, + containerColorOverride = containerColorOverride, + contentColorOverride = contentColorOverride, + ) { + if (loading) { + CircularProgressIndicator( + modifier = Modifier.size(iconSize), + strokeWidth = 2.dp, + color = LocalContentColor.current, + ) + } else if (leadingIcon != null) { + Icon( + imageVector = leadingIcon, + contentDescription = null, + modifier = Modifier.size(iconSize), + ) + } + Text( + text = label, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) + if (trailingIcon != null && !loading) { + Icon( + imageVector = trailingIcon, + contentDescription = null, + modifier = Modifier.size(iconSize), + ) + } + } +} diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt index a33d30a67..eb4cc5484 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/UpdatesBanner.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,11 +19,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Update -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,6 +36,7 @@ import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.apps_updates_banner_hide @@ -153,26 +151,17 @@ fun UpdatesBanner( leadingIcon = Icons.Default.Update, modifier = Modifier.weight(1f), ) - // TODO(ghs-button): needs onPrimaryContainer border/ink to match banner context - OutlinedButton( + GhsButton( onClick = onToggleExpanded, + label = if (isExpanded) { + stringResource(Res.string.apps_updates_banner_hide) + } else { + stringResource(Res.string.apps_updates_banner_show) + }, + variant = GhsButtonVariant.Outline, modifier = Modifier.height(44.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 18.dp), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.35f)), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - ) { - Text( - text = if (isExpanded) { - stringResource(Res.string.apps_updates_banner_hide) - } else { - stringResource(Res.string.apps_updates_banner_show) - }, - fontWeight = FontWeight.Medium, - ) - } + contentColorOverride = MaterialTheme.colorScheme.onPrimaryContainer, + ) } } } @@ -211,19 +200,14 @@ private fun UpdateAllInlineProgress( overflow = TextOverflow.Ellipsis, ) } - // TODO(ghs-button): needs onPrimaryContainer border/ink to match banner context - OutlinedButton( + GhsButton( onClick = onCancel, + label = stringResource(Res.string.cancel), + variant = GhsButtonVariant.Outline, + size = GhsButtonSize.Sm, modifier = Modifier.height(38.dp), - shape = RoundedCornerShape(50), - contentPadding = PaddingValues(horizontal = 14.dp), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.35f)), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - ) { - Text(stringResource(Res.string.cancel)) - } + contentColorOverride = MaterialTheme.colorScheme.onPrimaryContainer, + ) } Spacer(Modifier.height(10.dp)) LinearWavyProgressIndicator( diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 8b6f17477..12daf9a74 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -85,6 +85,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.auth.presentation.model.AuthLoginState import zed.rainxch.auth.presentation.model.GithubDeviceStartUi +import androidx.compose.ui.text.style.TextOverflow import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant @@ -668,7 +669,6 @@ private fun StateError( } } -// TODO(ghs-button): PrimaryPillButton wraps Button with composite Row(icon + text) content. @Composable private fun PrimaryPillButton( text: String, @@ -676,31 +676,21 @@ private fun PrimaryPillButton( leadingIcon: (@Composable () -> Unit)? = null, enabled: Boolean = true, ) { - Button( + GhsButton( onClick = onClick, enabled = enabled, + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Lg, modifier = Modifier .fillMaxWidth() .height(52.dp), - shape = RoundedCornerShape(50), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - contentPadding = PaddingValues(horizontal = 20.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - leadingIcon?.invoke() - Text( - text = text, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } + leadingIcon?.invoke() + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } @@ -844,38 +834,16 @@ private fun PatSignInSheet( } } - // TODO(ghs-button): composite content (CircularProgressIndicator + Text), needs custom width/height match - Button( + GhsButton( onClick = { onAction(AuthenticationAction.SubmitPat) }, + label = stringResource(Res.string.pat_submit), enabled = !isSubmitting && input.isNotBlank(), + loading = isSubmitting, + variant = GhsButtonVariant.Primary, modifier = Modifier .weight(1f) .height(48.dp), - shape = RoundedCornerShape(50), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (isSubmitting) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary, - ) - } - Text( - text = stringResource(Res.string.pat_submit), - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } - } + ) } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt index d4fe6c305..ccb77359e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt @@ -30,7 +30,9 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -164,16 +166,13 @@ private fun Coachmark(onDismiss: () -> Unit) { modifier = Modifier.padding(top = 2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { - // TODO(ghs-button): needs onPrimary text color in coachmark popup - TextButton(onClick = onDismiss) { - Text( - text = stringResource(Res.string.apk_inspect_coachmark_dismiss), - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.apk_inspect_coachmark_dismiss), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + contentColorOverride = MaterialTheme.colorScheme.onPrimary, + ) } } } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt index 51122de71..e1b47def3 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt @@ -30,7 +30,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -327,14 +329,13 @@ private fun ChannelChipCoachmark(onDismiss: () -> Unit) { modifier = Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = Arrangement.End, ) { - // TODO(ghs-button): needs onPrimary text color in coachmark popup, not standard primary - TextButton(onClick = onDismiss) { - Text( - text = stringResource(Res.string.channel_chip_coachmark_dismiss), - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.SemiBold, - ) - } + GhsButton( + onClick = onDismiss, + label = stringResource(Res.string.channel_chip_coachmark_dismiss), + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + contentColorOverride = MaterialTheme.colorScheme.onPrimary, + ) } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt index fc4678f0f..7b64a14e1 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt @@ -11,7 +11,6 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize @@ -60,19 +59,18 @@ fun SortByBottomSheet( ) { SortByUi.entries.forEach { option -> val isSelected = option == selectedSortBy - // TODO(ghs-button): needs per-item color (primary when selected, onSurface otherwise) - TextButton( - onClick = { - onSortBySelected(option) - }, + GhsButton( + onClick = { onSortBySelected(option) }, + label = stringResource(option.label()) + if (isSelected) " ✓" else "", + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(option.label()) + if (isSelected) " ✓" else "", - style = MaterialTheme.typography.bodyMedium, - color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - ) - } + contentColorOverride = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + ) } HorizontalDivider() From 73ef8db7d9c36734cf0936ecb9563aee292f319f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:36:38 +0500 Subject: [PATCH 110/172] feat(tweaks): paste full URL fast path for master proxy --- .../tweaks/presentation/TweaksAction.kt | 8 + .../tweaks/presentation/TweaksViewModel.kt | 12 ++ .../connection/PasteProxyUrlSheet.kt | 144 ++++++++++++++++++ .../connection/TweaksConnectionRoot.kt | 36 ++++- 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index 99c4de167..9f51b7294 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -222,6 +222,14 @@ sealed interface TweaksAction { data object OnMasterProxySave : TweaksAction data object OnMasterProxyTest : TweaksAction + data class OnMasterProxyPasteUrl( + val type: ProxyType, + val host: String, + val port: Int, + val username: String?, + val password: String?, + ) : TweaksAction + data class OnScopeUseMainToggled(val scope: ProxyScope, val useMain: Boolean) : TweaksAction data class OnTelemetryToggled(val enabled: Boolean) : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 2153546d4..911b43200 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -1215,6 +1215,18 @@ class TweaksViewModel( mutateMasterForm { it.copy(isPasswordVisible = !it.isPasswordVisible) } } + is TweaksAction.OnMasterProxyPasteUrl -> { + mutateMasterForm { + it.copy( + type = action.type, + host = action.host, + port = action.port.toString(), + username = action.username.orEmpty(), + password = action.password.orEmpty(), + ) + } + } + TweaksAction.OnMasterProxySave -> { val config = buildMasterProxyConfig() ?: return viewModelScope.launch { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt new file mode 100644 index 000000000..d50d5c091 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt @@ -0,0 +1,144 @@ +package zed.rainxch.tweaks.presentation.connection + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant +import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.tweaks.presentation.model.ProxyType + +data class PastedProxy( + val type: ProxyType, + val host: String, + val port: Int, + val username: String?, + val password: String?, +) + +fun parseProxyUrl(raw: String): PastedProxy? { + val trimmed = raw.trim() + if (trimmed.isEmpty()) return null + val schemeIdx = trimmed.indexOf("://") + if (schemeIdx <= 0) return null + val scheme = trimmed.substring(0, schemeIdx).lowercase() + val type = when (scheme) { + "http", "https" -> ProxyType.HTTP + "socks", "socks4", "socks5", "socks5h" -> ProxyType.SOCKS + else -> return null + } + val afterScheme = trimmed.substring(schemeIdx + 3) + .substringBefore('/') + .substringBefore('?') + .substringBefore('#') + if (afterScheme.isEmpty()) return null + + val atIdx = afterScheme.lastIndexOf('@') + val credPart = if (atIdx >= 0) afterScheme.substring(0, atIdx) else null + val hostPart = if (atIdx >= 0) afterScheme.substring(atIdx + 1) else afterScheme + + val (username, password) = if (credPart != null) { + val colonIdx = credPart.indexOf(':') + if (colonIdx >= 0) { + credPart.substring(0, colonIdx) to credPart.substring(colonIdx + 1) + } else { + credPart to "" + } + } else { + null to null + } + + val colonIdx = hostPart.lastIndexOf(':') + if (colonIdx <= 0) return null + val host = hostPart.substring(0, colonIdx) + val port = hostPart.substring(colonIdx + 1).toIntOrNull() ?: return null + if (host.isBlank() || port !in 1..65535) return null + + return PastedProxy( + type = type, + host = host, + port = port, + username = username?.takeIf { it.isNotBlank() }, + password = password?.takeIf { it.isNotBlank() }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PasteProxyUrlSheet( + onDismiss: () -> Unit, + onParsed: (PastedProxy) -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var input by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Paste proxy URL", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "Paste a full proxy URL and we'll fill the form for you.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + GhsTextField( + value = input, + onValueChange = { + input = it + error = null + }, + label = "scheme://user:pass@host:port", + isError = error != null, + supportingText = error, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(4.dp)) + GhsButton( + onClick = { + val parsed = parseProxyUrl(input) + if (parsed == null) { + error = "Couldn't read that URL." + } else { + onParsed(parsed) + } + }, + label = "Use this URL", + variant = GhsButtonVariant.Primary, + modifier = Modifier.fillMaxWidth(), + enabled = input.isNotBlank(), + ) + Spacer(Modifier.height(8.dp)) + } + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index bbb2b88d2..864295f64 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -25,10 +25,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material.icons.outlined.ContentPaste import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,6 +48,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.domain.model.ProxyScope import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsPasswordVisibilityIcon import zed.rainxch.core.presentation.components.inputs.GhsTextField @@ -75,6 +80,7 @@ fun TweaksConnectionRoot( val state by viewModel.state.collectAsStateWithLifecycle() val snackbarState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + var pasteSheetOpen by rememberSaveable { mutableStateOf(false) } ObserveAsEvents(viewModel.events) { event -> when (event) { @@ -114,6 +120,7 @@ fun TweaksConnectionRoot( MainConnectionCard( form = state.masterProxyForm, onAction = { viewModel.onAction(it) }, + onPasteUrl = { pasteSheetOpen = true }, ) Spacer(Modifier.height(16.dp)) } @@ -125,6 +132,24 @@ fun TweaksConnectionRoot( ) } } + + if (pasteSheetOpen) { + PasteProxyUrlSheet( + onDismiss = { pasteSheetOpen = false }, + onParsed = { parsed -> + viewModel.onAction( + TweaksAction.OnMasterProxyPasteUrl( + type = parsed.type, + host = parsed.host, + port = parsed.port, + username = parsed.username, + password = parsed.password, + ), + ) + pasteSheetOpen = false + }, + ) + } } @Composable @@ -157,6 +182,7 @@ private fun IntroCard() { private fun MainConnectionCard( form: ProxyScopeFormState, onAction: (TweaksAction) -> Unit, + onPasteUrl: () -> Unit, ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -196,7 +222,15 @@ private fun MainConnectionCard( style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(8.dp)) + GhsButton( + onClick = onPasteUrl, + label = "Paste full URL", + variant = GhsButtonVariant.Text, + size = GhsButtonSize.Sm, + leadingIcon = Icons.Outlined.ContentPaste, + ) + Spacer(Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), From 1611c3501f6ef2a5d884b63a3c4a148911123e65 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:38:54 +0500 Subject: [PATCH 111/172] feat(tweaks): 3-endpoint parallel master proxy test --- .../core/data/network/ProxyTesterImpl.kt | 7 ++-- .../core/domain/network/ProxyTester.kt | 2 ++ .../tweaks/presentation/TweaksEvent.kt | 6 ++++ .../tweaks/presentation/TweaksViewModel.kt | 35 ++++++++++++++++--- .../connection/TweaksConnectionRoot.kt | 8 +++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt index a3b8a1156..1535ba225 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyTesterImpl.kt @@ -16,7 +16,10 @@ import kotlin.coroutines.cancellation.CancellationException import kotlin.time.TimeSource class ProxyTesterImpl : ProxyTester { - override suspend fun test(config: ProxyConfig): ProxyTestOutcome { + override suspend fun test(config: ProxyConfig): ProxyTestOutcome = + test(config, TEST_URL) + + override suspend fun test(config: ProxyConfig, url: String): ProxyTestOutcome { val client = createPlatformHttpClient(config).config { install(HttpTimeout) { @@ -29,7 +32,7 @@ class ProxyTesterImpl : ProxyTester { return try { val started = TimeSource.Monotonic.markNow() - val response: HttpResponse = client.get(TEST_URL) + val response: HttpResponse = client.get(url) val elapsed = started.elapsedNow().inWholeMilliseconds when { diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt index 88438bd96..45fb69064 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/network/ProxyTester.kt @@ -4,6 +4,8 @@ import zed.rainxch.core.domain.model.ProxyConfig interface ProxyTester { suspend fun test(config: ProxyConfig): ProxyTestOutcome + + suspend fun test(config: ProxyConfig, url: String): ProxyTestOutcome = test(config) } sealed interface ProxyTestOutcome { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt index 55e5177c9..21bc997f8 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt @@ -15,6 +15,12 @@ sealed interface TweaksEvent { val message: String, ) : TweaksEvent + data class OnMasterProxyTestResult( + val searchMs: Long?, + val downloadMs: Long?, + val translationMs: Long?, + ) : TweaksEvent + data object OnCacheCleared : TweaksEvent data class OnCacheClearError( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 911b43200..f3a164588 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -1263,12 +1264,23 @@ class TweaksViewModel( it.copy(masterProxyForm = it.masterProxyForm.copy(isTestInProgress = true)) } viewModelScope.launch { - val outcome: ProxyTestOutcome = try { - proxyTester.test(config) + val results = try { + kotlinx.coroutines.coroutineScope { + val search = async { + runProbe(config, "https://api.github.com/zen") + } + val download = async { + runProbe(config, "https://github.com/robots.txt") + } + val translation = async { + runProbe(config, "https://translate.disroot.org") + } + Triple(search.await(), download.await(), translation.await()) + } } catch (e: CancellationException) { throw e } catch (e: Exception) { - ProxyTestOutcome.Failure.Unknown(e.message) + Triple(null, null, null) } finally { _state.update { it.copy( @@ -1276,7 +1288,13 @@ class TweaksViewModel( ) } } - _events.send(outcome.toEvent()) + _events.send( + TweaksEvent.OnMasterProxyTestResult( + searchMs = results.first, + downloadMs = results.second, + translationMs = results.third, + ), + ) } } @@ -1331,6 +1349,15 @@ class TweaksViewModel( } } + private suspend fun runProbe(config: ProxyConfig, url: String): Long? = try { + val outcome = proxyTester.test(config, url) + (outcome as? ProxyTestOutcome.Success)?.latencyMs + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + null + } + private fun buildMasterProxyConfigForTest(): ProxyConfig? { val form = _state.value.masterProxyForm return when (form.type) { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index 864295f64..d6709fa02 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -98,6 +98,14 @@ fun TweaksConnectionRoot( is TweaksEvent.OnProxyTestError -> coroutineScope.launch { snackbarState.showSnackbar(event.message) } + is TweaksEvent.OnMasterProxyTestResult -> coroutineScope.launch { + val parts = listOfNotNull( + "Search ${event.searchMs?.let { "✓ ${it}ms" } ?: "✗"}", + "Downloads ${event.downloadMs?.let { "✓ ${it}ms" } ?: "✗"}", + "Translation ${event.translationMs?.let { "✓ ${it}ms" } ?: "✗"}", + ) + snackbarState.showSnackbar(parts.joinToString(" · ")) + } else -> Unit } } From fe0d4b6fdb980876fdaeee16fa0140cbbab33144 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:39:56 +0500 Subject: [PATCH 112/172] feat(core): v2 master proxy migration with plurality vote --- .../data/repository/ProxyRepositoryImpl.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt index 6a20eda94..ca9acb2b4 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt @@ -43,6 +43,7 @@ class ProxyRepositoryImpl( init { migrationScope.launch { runCatching { migrateIfNeeded() } + runCatching { migrateMasterV2IfNeeded() } migrationDeferred.complete(Unit) } } @@ -310,7 +311,48 @@ class ProxyRepositoryImpl( } } + private suspend fun migrateMasterV2IfNeeded() { + val alreadyDone = runCatching { + ksafe.safeGet(MIGRATION_MARKER_MASTER_V2, false) + }.getOrDefault(false) + if (alreadyDone) return + + val configs = ProxyScope.entries.map { scope -> + scope to readScopeConfigDirect(scope) + } + + // Plurality vote: most common scope config becomes the master. + // Tie-break: Download > Discovery > Translation (most-common scope usage order). + val tieBreakOrder = listOf(ProxyScope.DOWNLOAD, ProxyScope.DISCOVERY, ProxyScope.TRANSLATION) + val counts = configs.groupBy { it.second }.mapValues { it.value.size } + val maxCount = counts.values.maxOrNull() ?: 0 + val winners = counts.filter { it.value == maxCount }.keys + val winnerConfig = tieBreakOrder.firstNotNullOfOrNull { scope -> + configs.firstOrNull { it.first == scope && it.second in winners }?.second + } ?: configs.first().second + + runCatching { setMasterProxyConfig(winnerConfig) } + + configs.forEach { (scope, config) -> + val matches = config == winnerConfig + runCatching { setUseMaster(scope, matches) } + } + + runCatching { ksafe.safePut(MIGRATION_MARKER_MASTER_V2, true) } + } + + private suspend fun readScopeConfigDirect(scope: ProxyScope): ProxyConfig { + val keys = keysFor(scope) + val type = runCatching { ksafe.safeGet(keys.type, null) }.getOrNull() + val host = runCatching { ksafe.safeGet(keys.host, null) }.getOrNull() + val port = runCatching { ksafe.safeGet(keys.port, null) }.getOrNull() + val user = runCatching { ksafe.safeGet(keys.username, null) }.getOrNull() + val pass = runCatching { ksafe.safeGet(keys.password, null) }.getOrNull() + return parseConfig(type, host, port, user, pass) + } + private companion object { const val MIGRATION_MARKER = "__migrated_proxy_v1__" + const val MIGRATION_MARKER_MASTER_V2 = "__migrated_proxy_master_v2__" } } From 63011c4c97f75b7b87ef0a85ff19a0b96d1c33b9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sat, 23 May 2026 22:41:42 +0500 Subject: [PATCH 113/172] feat(tweaks): plural strings for custom forges and other counts --- .../composeResources/values/strings.xml | 16 ++++++++++++++++ .../presentation/sources/TweaksSourcesRoot.kt | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 94de924af..71a8e02ec 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -841,6 +841,22 @@ Showing %1$d of %2$d asset Showing %1$d of %2$d assets + + %1$d host + %1$d hosts + + + %1$d token + %1$d tokens + + + %1$d app + %1$d apps + + + %1$d repo + %1$d repos + Fall back to older releases Walk back through past releases until one matches the filter. Required for monorepos where the latest release belongs to a sibling app. Advanced filter diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt index db61fb1b2..4838ac125 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -36,11 +36,13 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.custom_forges_count import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_sources import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel @@ -110,7 +112,7 @@ fun TweaksSourcesRoot( subtitle = if (count == 0) { "Add a Forgejo or Gitea host" } else { - "$count host${if (count == 1) "" else "s"}" + pluralStringResource(Res.plurals.custom_forges_count, count, count) }, accent = TweaksAccents.Mint, onClick = { viewModel.onAction(TweaksAction.OnOpenCustomForgesDialog) }, From 0a69b19c7b757c7d16338e9ce56c0a44febddea0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 10:32:35 +0500 Subject: [PATCH 114/172] feat(p14): outlined Radii.row vocab for ExpressiveCard, consistent topbars --- .../presentation/components/ExpressiveCard.kt | 73 ++++++------------- .../favourites/presentation/FavouritesRoot.kt | 8 +- .../presentation/RecentlyViewedRoot.kt | 8 +- .../starred/presentation/StarredReposRoot.kt | 13 ++-- 4 files changed, 37 insertions(+), 65 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt index 9680379a0..5c44b7d80 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt @@ -1,18 +1,17 @@ package zed.rainxch.core.presentation.components +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp - -private val EXPRESSIVE_CARD_SHAPE = RoundedCornerShape(32.dp) +import zed.rainxch.core.presentation.theme.tokens.Radii @OptIn(ExperimentalFoundationApi::class) @Composable @@ -22,54 +21,26 @@ fun ExpressiveCard( onLongClick: (() -> Unit)? = null, content: @Composable () -> Unit, ) { - check(onLongClick == null || onClick != null) { "ExpressiveCard: onLongClick requires onClick" } - when { - onClick != null && onLongClick != null -> { - - ElevatedCard( - modifier = - modifier - .fillMaxWidth() - .clip(EXPRESSIVE_CARD_SHAPE) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = EXPRESSIVE_CARD_SHAPE, - content = { content() }, - ) - } - - onClick != null -> { - ElevatedCard( - modifier = modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - onClick = onClick, - shape = EXPRESSIVE_CARD_SHAPE, - content = { content() }, - ) - } - - else -> { - ElevatedCard( - modifier = modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = EXPRESSIVE_CARD_SHAPE, - content = { content() }, - ) - } + val baseModifier = modifier.fillMaxWidth().clip(Radii.row) + val clickModifier = when { + onClick != null && onLongClick != null -> baseModifier.combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + onClick != null -> baseModifier.combinedClickable(onClick = onClick) + else -> baseModifier } + Surface( + modifier = clickModifier, + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + ), + content = content, + ) } diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt index 31540e24b..60e2bed3d 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/FavouritesRoot.kt @@ -196,14 +196,14 @@ private fun FavouritesTopbar( title = { Text( text = stringResource(Res.string.favourites), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { IconButton( - shapes = IconButtonDefaults.shapes(), onClick = { onAction(FavouritesAction.OnNavigateBackClick) }, diff --git a/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/RecentlyViewedRoot.kt b/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/RecentlyViewedRoot.kt index df21ad0f2..d9a25017a 100644 --- a/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/RecentlyViewedRoot.kt +++ b/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/RecentlyViewedRoot.kt @@ -143,14 +143,14 @@ private fun RecentlyViewedTopbar(onAction: (RecentlyViewedAction) -> Unit) { title = { Text( text = stringResource(Res.string.recently_viewed), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { IconButton( - shapes = IconButtonDefaults.shapes(), onClick = { onAction(RecentlyViewedAction.OnNavigateBackClick) }, diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt index 62f27e30c..035d2d9c8 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt @@ -291,9 +291,10 @@ private fun StarredTopBar( Column { Text( text = stringResource(Res.string.starred_repositories), - style = MaterialTheme.typography.titleMediumEmphasized, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) if (lastSyncTime != null && !isSyncing) { @@ -309,7 +310,6 @@ private fun StarredTopBar( }, navigationIcon = { IconButton( - shapes = IconButtonDefaults.shapes(), onClick = { onAction(StarredReposAction.OnNavigateBackClick) }, ) { Icon( @@ -446,9 +446,10 @@ private fun EmptyStateContent( if (actionText != null && onActionClick != null) { Spacer(modifier = Modifier.height(16.dp)) - GithubStoreButton( - text = actionText, + GhsButton( onClick = onActionClick, + label = actionText, + variant = GhsButtonVariant.Primary, ) } } From 5cadf965171e9a2ab763f1e23301d6334fe8925c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 10:37:56 +0500 Subject: [PATCH 115/172] feat(p14): outlined row vocab + StatChip for Favourites Starred RecentlyViewed items --- .../components/FavouriteRepositoryItem.kt | 197 ++++++------ .../components/RecentlyViewedItem.kt | 156 ++++----- .../components/StarredRepositoryItem.kt | 303 ++++++++---------- 3 files changed, 306 insertions(+), 350 deletions(-) diff --git a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt index 2faf67eba..a1f4ac109 100644 --- a/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt +++ b/feature/favourites/presentation/src/commonMain/kotlin/zed/rainxch/favourites/presentation/components/FavouriteRepositoryItem.kt @@ -1,10 +1,11 @@ package zed.rainxch.favourites.presentation.components +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -12,25 +13,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CalendarToday import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.NewReleases -import androidx.compose.material3.AssistChip -import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.CircularWavyProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,14 +34,18 @@ import androidx.compose.ui.unit.dp import com.skydoves.landscapist.coil3.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.crossfade.CrossfadePlugin +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.components.OfficialBadge +import zed.rainxch.core.presentation.components.chips.StatChip +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.favourites.presentation.model.FavouriteRepository import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.remove_from_favourites -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class) @Composable fun FavouriteRepositoryItem( favouriteRepository: FavouriteRepository, @@ -62,26 +59,22 @@ fun FavouriteRepositoryItem( onClick = onItemClick, ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), ) { Row( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(24.dp)) - .clickable(onClick = onDevProfileClick), + modifier = Modifier + .fillMaxWidth() + .clip(Radii.chip) + .clickable(onClick = onDevProfileClick) + .padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { CoilImage( imageModel = { favouriteRepository.repoOwnerAvatarUrl }, - modifier = - Modifier - .size(32.dp) - .clip(CircleShape), + modifier = Modifier + .size(28.dp) + .clip(CircleShape), loading = { Box( modifier = Modifier.fillMaxSize(), @@ -90,148 +83,146 @@ fun FavouriteRepositoryItem( CircularWavyProgressIndicator() } }, - component = - rememberImageComponent { - CrossfadePlugin() - }, + component = rememberImageComponent { + CrossfadePlugin() + }, ) - Text( text = favouriteRepository.repoOwner, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) - if (favouriteRepository.isCurrentUserOwner) { OfficialBadge() } } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Column( - modifier = Modifier.weight(1f), - ) { + Column(modifier = Modifier.weight(1f)) { Text( text = favouriteRepository.repoName, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - favouriteRepository.repoDescription?.let { - Spacer(modifier = Modifier.height(4.dp)) - + Spacer(Modifier.height(4.dp)) Text( text = it, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis, ) } } - - IconButton( + FavoriteToggle( + favorited = true, onClick = onToggleFavouriteClick, - colors = IconButtonDefaults.filledTonalIconButtonColors(), - modifier = Modifier.align(Alignment.CenterVertically), - shape = MaterialShapes.Cookie6Sided.toShape(), - ) { - Icon( - imageVector = Icons.Default.Favorite, - contentDescription = stringResource(Res.string.remove_from_favourites), - ) - } + ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), + FlowRow( + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { favouriteRepository.primaryLanguage?.let { language -> - AssistChip( - onClick = { }, - label = { - Text( - text = language, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { + StatChip( + label = language, + leading = { Icon( imageVector = Icons.Default.Code, contentDescription = null, - modifier = Modifier.size(AssistChipDefaults.IconSize), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, - colors = - AssistChipDefaults.assistChipColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - labelColor = MaterialTheme.colorScheme.onPrimaryContainer, - leadingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), + background = MaterialTheme.colorScheme.surfaceContainerHigh, + border = MaterialTheme.colorScheme.outline, + contentColor = MaterialTheme.colorScheme.onSurface, ) } - favouriteRepository.latestRelease?.let { release -> - AssistChip( - onClick = { }, - label = { - Text( - text = release, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { + StatChip( + label = release, + leading = { Icon( imageVector = Icons.Default.NewReleases, contentDescription = null, - modifier = Modifier.size(AssistChipDefaults.IconSize), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.primary, ) }, + background = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + border = MaterialTheme.colorScheme.primary.copy(alpha = 0.30f), + contentColor = MaterialTheme.colorScheme.primary, ) } - - AssistChip( - onClick = { }, - label = { - Text( - text = favouriteRepository.addedAtFormatter, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { + StatChip( + label = favouriteRepository.addedAtFormatter, + leading = { Icon( imageVector = Icons.Default.CalendarToday, contentDescription = null, - modifier = Modifier.size(AssistChipDefaults.IconSize), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, + background = MaterialTheme.colorScheme.surfaceContainerHigh, + border = MaterialTheme.colorScheme.outline, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } } + +@Composable +private fun FavoriteToggle( + favorited: Boolean, + onClick: () -> Unit, +) { + val tint = if (favorited) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + val container = if (favorited) { + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.45f) + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(container), + contentAlignment = Alignment.Center, + ) { + IconButton(onClick = onClick, modifier = Modifier.size(38.dp)) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = stringResource(Res.string.remove_from_favourites), + tint = tint, + modifier = Modifier.size(18.dp), + ) + } + } +} diff --git a/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/components/RecentlyViewedItem.kt b/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/components/RecentlyViewedItem.kt index 4dfcbd003..3670ce7e0 100644 --- a/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/components/RecentlyViewedItem.kt +++ b/feature/recently-viewed/presentation/src/commonMain/kotlin/zed/rainxch/recentlyviewed/presentation/components/RecentlyViewedItem.kt @@ -1,10 +1,12 @@ package zed.rainxch.recentlyviewed.presentation.components +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -12,20 +14,15 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.outlined.Close -import androidx.compose.material3.AssistChip -import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,9 +36,11 @@ import com.skydoves.landscapist.coil3.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.crossfade.CrossfadePlugin import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.chips.StatChip +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.recentlyviewed.presentation.model.RecentlyViewedRepo -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class) @Composable fun RecentlyViewedItem( repo: RecentlyViewedRepo, @@ -55,26 +54,22 @@ fun RecentlyViewedItem( onClick = onItemClick, ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), ) { Row( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(24.dp)) - .clickable(onClick = onDevProfileClick), + modifier = Modifier + .fillMaxWidth() + .clip(Radii.chip) + .clickable(onClick = onDevProfileClick) + .padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { CoilImage( imageModel = { repo.repoOwnerAvatarUrl }, - modifier = - Modifier - .size(32.dp) - .clip(CircleShape), + modifier = Modifier + .size(28.dp) + .clip(CircleShape), loading = { Box( modifier = Modifier.fillMaxSize(), @@ -83,122 +78,111 @@ fun RecentlyViewedItem( CircularWavyProgressIndicator() } }, - component = - rememberImageComponent { - CrossfadePlugin() - }, + component = rememberImageComponent { + CrossfadePlugin() + }, ) - Text( text = repo.repoOwner, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Column( - modifier = Modifier.weight(1f), - ) { + Column(modifier = Modifier.weight(1f)) { Text( text = repo.repoName, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - repo.repoDescription?.let { - Spacer(modifier = Modifier.height(4.dp)) - + Spacer(Modifier.height(4.dp)) Text( text = it, - fontWeight = FontWeight.Medium, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis, ) } } - - IconButton( - onClick = onRemoveClick, - colors = IconButtonDefaults.filledTonalIconButtonColors(), - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = null, - ) - } + RemoveToggle(onClick = onRemoveClick) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), + FlowRow( + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { repo.primaryLanguage?.let { language -> - AssistChip( - onClick = { }, - label = { - Text( - text = language, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { + StatChip( + label = language, + leading = { Icon( imageVector = Icons.Default.Code, contentDescription = null, - modifier = Modifier.size(AssistChipDefaults.IconSize), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, - colors = - AssistChipDefaults.assistChipColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - labelColor = MaterialTheme.colorScheme.onPrimaryContainer, - leadingIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), + background = MaterialTheme.colorScheme.surfaceContainerHigh, + border = MaterialTheme.colorScheme.outline, + contentColor = MaterialTheme.colorScheme.onSurface, ) } - - AssistChip( - onClick = { }, - label = { - Text( - text = repo.viewedAtFormatted, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingIcon = { + StatChip( + label = repo.viewedAtFormatted, + leading = { Icon( imageVector = Icons.Default.Schedule, contentDescription = null, - modifier = Modifier.size(AssistChipDefaults.IconSize), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, + background = MaterialTheme.colorScheme.surfaceContainerHigh, + border = MaterialTheme.colorScheme.outline, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } } + +@Composable +private fun RemoveToggle(onClick: () -> Unit) { + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + IconButton(onClick = onClick, modifier = Modifier.size(38.dp)) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + } +} diff --git a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt index 457bffd52..f01704af2 100644 --- a/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt +++ b/feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/components/StarredRepositoryItem.kt @@ -1,8 +1,10 @@ package zed.rainxch.starred.presentation.components +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row @@ -11,32 +13,24 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.CallSplit +import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.Badge -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconToggleButton import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialShapes +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -45,12 +39,14 @@ import com.skydoves.landscapist.coil3.CoilImage import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.components.OfficialBadge +import zed.rainxch.core.presentation.components.chips.StatChip import zed.rainxch.core.presentation.theme.GithubStoreTheme +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.starred.presentation.model.StarredRepositoryUi -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun StarredRepositoryItem( repository: StarredRepositoryUi, @@ -64,48 +60,36 @@ fun StarredRepositoryItem( modifier = modifier.fillMaxWidth(), ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { CoilImage( imageModel = { repository.repoOwnerAvatarUrl }, - modifier = - Modifier - .size(40.dp) - .clip(CircleShape) - .clickable(onClick = { - onDevProfileClick() - }), - imageOptions = - ImageOptions( - contentScale = ContentScale.Crop, - ), + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .clickable(onClick = onDevProfileClick), + imageOptions = ImageOptions(contentScale = ContentScale.Crop), ) - Spacer(modifier = Modifier.width(12.dp)) - Column( - modifier = - Modifier - .weight(1f) - .clickable(onClick = { - onDevProfileClick() - }), + modifier = Modifier + .weight(1f) + .clickable(onClick = onDevProfileClick), ) { Text( text = repository.repoName, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -118,124 +102,69 @@ fun StarredRepositoryItem( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false), ) - if (repository.isCurrentUserOwner) { OfficialBadge() } } } - FilledIconToggleButton( - checked = repository.isFavorite, - onCheckedChange = { onToggleFavoriteClick() }, - modifier = Modifier.size(40.dp), - shape = MaterialShapes.Cookie6Sided.toShape(), - ) { - Icon( - imageVector = - if (repository.isFavorite) { - Icons.Filled.Favorite - } else { - Icons.Outlined.FavoriteBorder - }, - contentDescription = - if (repository.isFavorite) { - stringResource(Res.string.remove_from_favourites) - } else { - stringResource(Res.string.add_to_favourites) - }, - modifier = Modifier.size(20.dp), - ) - } + FavoriteToggle( + favorited = repository.isFavorite, + onClick = onToggleFavoriteClick, + ) } repository.repoDescription?.let { description -> - Spacer(modifier = Modifier.height(12.dp)) - + Spacer(Modifier.height(10.dp)) Text( text = description, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 3, overflow = TextOverflow.Ellipsis, ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - StatChip( + StatChipNeutral( icon = Icons.Default.Star, label = formatCount(repository.stargazersCount), - contentDescription = "${repository.stargazersCount} ${stringResource(Res.string.stars)}", ) - - StatChip( + StatChipNeutral( icon = Icons.AutoMirrored.Filled.CallSplit, label = formatCount(repository.forksCount), - contentDescription = "${repository.forksCount} ${stringResource(Res.string.forks)}", ) - if (repository.openIssuesCount > 0) { - StatChip( + StatChipNeutral( icon = Icons.Outlined.Warning, label = formatCount(repository.openIssuesCount), - contentDescription = "${repository.openIssuesCount} ${stringResource(Res.string.issues)}", ) } - repository.primaryLanguage?.let { language -> - SuggestionChip( - onClick = {}, - label = { - Text( - text = language, - style = MaterialTheme.typography.labelSmall, - ) - }, - modifier = Modifier.height(32.dp), + StatChipNeutral( + icon = Icons.Default.Code, + label = language, ) } - } - - if (repository.isInstalled || repository.latestRelease != null) { - Spacer(modifier = Modifier.height(12.dp)) - - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - if (repository.isInstalled) { - Badge( - containerColor = MaterialTheme.colorScheme.primaryContainer, - ) { - Text( - text = stringResource(Res.string.installed), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer, - ) - } - } - - repository.latestRelease?.let { version -> - Badge( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - ) { - Text( - text = version, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } - } + if (repository.isInstalled) { + TonalBadge( + text = stringResource(Res.string.installed), + container = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.18f), + content = MaterialTheme.colorScheme.tertiary, + ) + } + repository.latestRelease?.let { version -> + TonalBadge( + text = version, + container = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), + content = MaterialTheme.colorScheme.primary, + ) } } } @@ -243,56 +172,108 @@ fun StarredRepositoryItem( } @Composable -private fun StatChip( - icon: ImageVector, - label: String, - contentDescription: String, - modifier: Modifier = Modifier, +private fun StatChipNeutral(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String) { + StatChip( + label = label, + leading = { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + background = MaterialTheme.colorScheme.surfaceContainerHigh, + border = MaterialTheme.colorScheme.outline, + contentColor = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable +private fun TonalBadge( + text: String, + container: androidx.compose.ui.graphics.Color, + content: androidx.compose.ui.graphics.Color, ) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, + Box( + modifier = Modifier + .clip(Radii.chip) + .background(container) + .padding(horizontal = 10.dp, vertical = 5.dp), ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = label, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, + text = text, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, ) } } +@Composable +private fun FavoriteToggle( + favorited: Boolean, + onClick: () -> Unit, +) { + val tint = if (favorited) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + val container = if (favorited) { + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.45f) + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(container), + contentAlignment = Alignment.Center, + ) { + IconButton(onClick = onClick, modifier = Modifier.size(38.dp)) { + Icon( + imageVector = if (favorited) { + Icons.Filled.Favorite + } else { + Icons.Outlined.FavoriteBorder + }, + contentDescription = if (favorited) { + stringResource(Res.string.remove_from_favourites) + } else { + stringResource(Res.string.add_to_favourites) + }, + tint = tint, + modifier = Modifier.size(18.dp), + ) + } + } +} + @Preview @Composable private fun PreviewStarredRepoItem() { GithubStoreTheme { StarredRepositoryItem( - repository = - StarredRepositoryUi( - repoId = 1, - repoName = "awesome-app", - repoOwner = "developer", - repoOwnerAvatarUrl = "", - repoDescription = "An awesome application that does amazing things", - primaryLanguage = "Kotlin", - repoUrl = "", - stargazersCount = 1234, - forksCount = 567, - openIssuesCount = 12, - isInstalled = true, - isFavorite = false, - latestRelease = "v1.2.3", - latestReleaseUrl = null, - starredAt = null, - ), + repository = StarredRepositoryUi( + repoId = 1, + repoName = "awesome-app", + repoOwner = "developer", + repoOwnerAvatarUrl = "", + repoDescription = "An awesome application that does amazing things", + primaryLanguage = "Kotlin", + repoUrl = "", + stargazersCount = 1234, + forksCount = 567, + openIssuesCount = 12, + isInstalled = true, + isFavorite = false, + latestRelease = "v1.2.3", + latestReleaseUrl = null, + starredAt = null, + ), onToggleFavoriteClick = {}, onItemClick = {}, onDevProfileClick = {}, From d04ea0e70bc6bf430bfa915a406f4f55ff2e37c0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 12:12:44 +0500 Subject: [PATCH 116/172] refactor(p15): outlined row vocab and Ghs primitives across sub-screens --- .../announcements/AnnouncementsRoot.kt | 6 ++- .../whatsnew/WhatsNewHistoryScreen.kt | 6 ++- .../components/whatsnew/WhatsNewSheet.kt | 9 ++-- .../presentation/import/ExternalImportRoot.kt | 6 ++- .../import/components/CandidateCard.kt | 7 ++- .../presentation/starred/StarredPickerRoot.kt | 5 +- .../hidden/HiddenRepositoriesRoot.kt | 22 ++++---- .../presentation/hosttokens/HostTokensRoot.kt | 52 ++++++++++++------- .../presentation/mirror/MirrorPickerRoot.kt | 6 ++- .../skipped/SkippedUpdatesRoot.kt | 22 ++++---- 10 files changed, 87 insertions(+), 54 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementsRoot.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementsRoot.kt index ba232b7d4..23c24a41b 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementsRoot.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/announcements/AnnouncementsRoot.kt @@ -72,8 +72,10 @@ fun AnnouncementsRoot( title = { Text( text = stringResource(Res.string.announcements_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt index 66875c3c3..02a4cc416 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt @@ -40,8 +40,10 @@ fun WhatsNewHistoryScreen( title = { Text( text = stringResource(Res.string.whats_new_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt index 5caaac6d3..ae4a26c29 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt @@ -1,5 +1,6 @@ package zed.rainxch.core.presentation.components.whatsnew +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -26,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.WhatsNewEntry +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.domain.model.WhatsNewSection import zed.rainxch.core.domain.model.WhatsNewSectionType import zed.rainxch.githubstore.core.presentation.res.Res @@ -117,9 +119,10 @@ private fun SheetHeader(entry: WhatsNewEntry) { @Composable fun WhatsNewEntryCard(entry: WhatsNewEntry) { Surface( - shape = RoundedCornerShape(24.dp), - color = MaterialTheme.colorScheme.surfaceContainerLow, modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Column( modifier = Modifier.padding(16.dp), @@ -182,7 +185,7 @@ private fun SectionLabel(type: WhatsNewSectionType) { } Text( - text = label.uppercase(), + text = label, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = color, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt index 81878200a..f01b0b8c3 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/ExternalImportRoot.kt @@ -107,8 +107,10 @@ fun ExternalImportRoot( title = { Text( text = stringResource(Res.string.external_import_top_bar_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt index 6febbe559..c70b15f9a 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/import/components/CandidateCard.kt @@ -1,5 +1,6 @@ package zed.rainxch.apps.presentation.import.components +import androidx.compose.foundation.BorderStroke import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -22,6 +23,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant +import zed.rainxch.core.presentation.theme.tokens.Radii import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -71,12 +73,13 @@ fun CandidateCard( val reducedMotion = LocalReducedMotion.current Surface( - tonalElevation = 1.dp, - shape = RoundedCornerShape(20.dp), + shape = Radii.row, color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), modifier = modifier .fillMaxWidth() + .clip(Radii.row) .clickable( onClickLabel = if (expanded) collapseLabel else expandLabel, role = Role.Button, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt index 25aa05699..acbbbda68 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/starred/StarredPickerRoot.kt @@ -90,7 +90,10 @@ private fun StarredPickerScreen( title = { Text( text = stringResource(Res.string.starred_picker_title), - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt index 5963338a9..3f0377c1b 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt @@ -12,20 +12,19 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize @@ -47,6 +46,7 @@ import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_count @@ -100,8 +100,10 @@ fun HiddenRepositoriesRoot( Column { Text( text = stringResource(Res.string.hidden_repositories_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) if (state.items.isNotEmpty()) { Text( @@ -195,13 +197,11 @@ private fun HiddenRepoRow( item: HiddenRepoUi, onUnhide: () -> Unit, ) { - OutlinedCard( - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(24.dp), + Surface( modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Row( modifier = Modifier.padding(12.dp), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt index 67827f1d6..7cae4be97 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt @@ -1,5 +1,7 @@ package zed.rainxch.tweaks.presentation.hosttokens +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,7 +16,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add @@ -25,21 +26,19 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -50,6 +49,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -65,6 +65,7 @@ import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.host_tokens_action_add @@ -136,7 +137,15 @@ fun HostTokensRoot( Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(Res.string.host_tokens_title)) }, + title = { + Text( + text = stringResource(Res.string.host_tokens_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, + ) + }, navigationIcon = { IconButton(onClick = onNavigateBack) { Icon( @@ -246,12 +255,11 @@ private fun visiblePresetForges(state: HostTokensState): List { @Composable private fun OAuthCoexistenceNote() { - OutlinedCard( + Surface( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(16.dp), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Row( modifier = Modifier @@ -324,9 +332,11 @@ private fun PresetForgeCard( onPick: () -> Unit, onOpenTokenCreationPage: () -> Unit, ) { - ElevatedCard( + Surface( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Column( modifier = Modifier @@ -380,10 +390,14 @@ private fun PresetForgeCard( @Composable private fun OtherForgeCard(onPick: () -> Unit) { - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - onClick = onPick, + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(Radii.row) + .clickable(onClick = onPick), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Row( modifier = Modifier @@ -459,9 +473,11 @@ private fun TokenRow( ) { val forge = ForgeKind.fromHost(token.host) var menuOpen by remember { mutableStateOf(false) } - ElevatedCard( + Surface( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Row( modifier = Modifier diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt index 6da4aa6fd..b70383646 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/mirror/MirrorPickerRoot.kt @@ -90,8 +90,10 @@ fun MirrorPickerRoot( title = { Text( text = stringResource(Res.string.mirror_picker_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt index 147a932e7..5c2effb8e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/skipped/SkippedUpdatesRoot.kt @@ -11,23 +11,23 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.BorderStroke import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant +import zed.rainxch.core.presentation.theme.tokens.Radii import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -88,8 +88,10 @@ fun SkippedUpdatesRoot( title = { Text( text = stringResource(Res.string.skipped_updates_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onBackground, ) }, navigationIcon = { @@ -167,13 +169,11 @@ private fun SkippedAppRow( item: SkippedAppUi, onUnskip: () -> Unit, ) { - OutlinedCard( - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - ), - shape = RoundedCornerShape(24.dp), + Surface( modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Row( modifier = Modifier.padding(12.dp), From 01fe6b0738e3abf7999732712da46b7528ecf46a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 12:22:11 +0500 Subject: [PATCH 117/172] feat(p16): outlined section containers and accent tile in ApkInspectSheet --- .../components/ApkInspectSheet.kt | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt index 3f5b88ca5..42bcdc696 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt @@ -1,5 +1,7 @@ package zed.rainxch.details.presentation.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -49,9 +51,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import androidx.compose.ui.draw.clip import zed.rainxch.core.domain.model.ApkInspection import zed.rainxch.core.domain.model.ApkPermission import zed.rainxch.core.domain.model.ProtectionLevel +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.apk_inspect_compatibility import zed.rainxch.githubstore.core.presentation.res.apk_inspect_components @@ -157,8 +161,9 @@ private fun Header(inspection: ApkInspection) { ) Text( text = inspection.appLabel, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -364,10 +369,11 @@ private fun PermissionGroupHeader( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = label.uppercase(), - style = MaterialTheme.typography.labelMedium, + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), color = color, - fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f, fill = false), ) Surface( @@ -518,21 +524,42 @@ private fun InspectSection( ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) + Box( + modifier = Modifier + .size(32.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(10.dp)) Text( text = title, - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, ) } - content() + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + content() + } + } } } @@ -564,8 +591,9 @@ private fun InspectRow(label: String, value: String, monospace: Boolean = false) @Composable private fun DangerNote(text: String) { Surface( - shape = RoundedCornerShape(12.dp), + shape = Radii.chip, color = MaterialTheme.colorScheme.errorContainer, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.35f)), ) { Row( verticalAlignment = Alignment.CenterVertically, From f7a3d2291d1824a42fb29ffbce749798746bf548 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 13:09:24 +0500 Subject: [PATCH 118/172] feat(p16): trim sheet padding, long-press copy on identity fields --- .../components/ApkInspectSheet.kt | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt index 42bcdc696..4ac66a7f5 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt @@ -1,8 +1,10 @@ package zed.rainxch.details.presentation.components import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -35,6 +37,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.AnnotatedString import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -137,7 +143,7 @@ private fun EmptyState() { private fun InspectionContent(inspection: ApkInspection) { LazyColumn( modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { item { Header(inspection) } @@ -167,12 +173,14 @@ private fun Header(inspection: ApkInspection) { color = MaterialTheme.colorScheme.onSurface, maxLines = 2, overflow = TextOverflow.Ellipsis, + modifier = Modifier.copyableOnLongPress(inspection.appLabel), ) Text( text = inspection.packageName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, fontFamily = FontFamily.Monospace, + modifier = Modifier.copyableOnLongPress(inspection.packageName), ) val sourceLabel = when (inspection.source) { ApkInspection.Source.FILE -> stringResource(Res.string.apk_inspect_source_file) @@ -435,6 +443,7 @@ private fun PermissionRow(permission: ApkPermission) { fontFamily = FontFamily.Monospace, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.copyableOnLongPress(permission.name), ) } Surface( @@ -578,12 +587,17 @@ private fun InspectRow(label: String, value: String, monospace: Boolean = false) modifier = Modifier.width(120.dp), ) } + val valueModifier = if (monospace) { + Modifier.weight(1f).copyableOnLongPress(value) + } else { + Modifier.weight(1f) + } Text( text = value, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, fontFamily = if (monospace) FontFamily.Monospace else FontFamily.Default, - modifier = Modifier.weight(1f), + modifier = valueModifier, ) } } @@ -632,6 +646,20 @@ private fun protectionStyle(level: ProtectionLevel): Pair { } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun Modifier.copyableOnLongPress(value: String): Modifier { + val clipboard = LocalClipboardManager.current + val haptic = LocalHapticFeedback.current + return this.combinedClickable( + onClick = {}, + onLongClick = { + clipboard.setText(AnnotatedString(value)) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + ) +} + private fun formatBytes(bytes: Long): String = when { bytes >= 1_073_741_824 -> "%.1f GB".format(bytes / 1_073_741_824.0) From 48ef92d44979a571a5c2c0efeec23466a5a2325f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 13:41:55 +0500 Subject: [PATCH 119/172] i18n: externalize Tweaks redesign hardcoded English strings --- .../composeResources/values/strings.xml | 82 +++++++++++++++++++ .../presentation/appinfo/TweaksAppInfoRoot.kt | 30 ++++--- .../connection/PasteProxyUrlSheet.kt | 18 ++-- .../connection/TweaksConnectionRoot.kt | 67 +++++++++++---- .../language/TweaksLanguageRoot.kt | 3 +- .../presentation/licenses/LicensesRoot.kt | 11 ++- .../presentation/privacy/TweaksPrivacyRoot.kt | 66 ++++++++++----- .../presentation/sources/TweaksSourcesRoot.kt | 24 ++++-- .../presentation/storage/TweaksStorageRoot.kt | 18 ++-- 9 files changed, 246 insertions(+), 73 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 71a8e02ec..875bbfbf2 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1401,4 +1401,86 @@ Add token unknown error Couldn't restore token for %1$s + + + How the app reaches the internet + Pick a connection mode below. Most people leave this on No proxy. + Main connection + Per-scope overrides + Each scope uses the main connection by default. Choose \'Custom\' to keep its own settings. + Applies to all traffic unless overridden below. + Paste full URL + Paste proxy URL + Paste a full proxy URL and we\'ll fill the form for you. + scheme://user:pass@host:port + Couldn\'t read that URL. + Use this URL + Search & metadata + GitHub API, search, repo details. + Downloads + APK and asset downloads. + Translation + DeepL, Microsoft, LibreTranslate calls. + Use main + Custom + Test + No proxy + System + HTTP/HTTPS + SOCKS5 + Most corporate proxies. + Tor, SSH tunnels. + + Where the app looks for repositories + The app searches GitHub by default. Route through a regional mirror or add custom Forgejo / Gitea hosts. + Added hosts + Default (github.com) + Add a Forgejo or Gitea host + GitHub mirror + + Browsing history + Usage data + Share anonymous usage data + Help us understand which features get used. + What we collect + App version. + OS and platform. + Feature usage counts. + No repo names. + No tokens. + No identifiers. + Detect repo links in clipboard + When you copy a github.com or codeberg.org link, we\'ll prompt to open it. + Hide repos I\'ve already viewed + Skip seen repos in feeds and search. + Clear viewed history + Forget which repos you\'ve already opened. + Clear viewed history? + This won\'t unstar or unfavorite anything. + Clear + Hidden repositories + Repos you\'ve muted from feeds and search. + + Downloaded APKs + We keep installers around so updates resume fast. + Using: %1$s + Clear + 0 B + + GitHub Store + Cross-platform app store for GitHub, Codeberg, and Forgejo releases. + What\'s new + Past release notes. + Open source licenses + Libraries used in the app. + Privacy policy + View on github-store.org. + Source code on GitHub + View this app\'s source. + + The app restarts when you switch language. + + Open source licenses + GitHub Store stands on these libraries. + Tap any entry to open its project page. diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index 8e9e27425..5f7cb3528 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -43,6 +43,16 @@ import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_app_name +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_licenses_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_licenses_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_privacy_policy_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_privacy_policy_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_source_code_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_source_code_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_tagline +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_whats_new_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_whats_new_title import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_app_info import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel @@ -79,8 +89,8 @@ fun TweaksAppInfoRoot( item(key = "action_whats_new") { ActionRow( icon = Icons.Outlined.NewReleases, - title = "What's new", - subtitle = "Past release notes.", + title = stringResource(Res.string.tweaks_app_info_whats_new_title), + subtitle = stringResource(Res.string.tweaks_app_info_whats_new_subtitle), accent = TweaksAccents.Peach, onClick = onNavigateToWhatsNewHistory, ) @@ -90,8 +100,8 @@ fun TweaksAppInfoRoot( item(key = "action_licenses") { ActionRow( icon = Icons.Outlined.Code, - title = "Open source licenses", - subtitle = "Libraries used in the app.", + title = stringResource(Res.string.tweaks_app_info_licenses_title), + subtitle = stringResource(Res.string.tweaks_app_info_licenses_subtitle), accent = TweaksAccents.Sage, onClick = onNavigateToLicenses, ) @@ -101,8 +111,8 @@ fun TweaksAppInfoRoot( item(key = "action_privacy") { ActionRow( icon = Icons.Outlined.Description, - title = "Privacy policy", - subtitle = "View on github-store.org.", + title = stringResource(Res.string.tweaks_app_info_privacy_policy_title), + subtitle = stringResource(Res.string.tweaks_app_info_privacy_policy_subtitle), accent = TweaksAccents.Rose, onClick = { runCatching { uriHandler.openUri(PRIVACY_POLICY_URL) } @@ -114,8 +124,8 @@ fun TweaksAppInfoRoot( item(key = "action_source") { ActionRow( icon = Icons.AutoMirrored.Outlined.OpenInNew, - title = "Source code on GitHub", - subtitle = "View this app's source.", + title = stringResource(Res.string.tweaks_app_info_source_code_title), + subtitle = stringResource(Res.string.tweaks_app_info_source_code_subtitle), accent = TweaksAccents.Aqua, onClick = { runCatching { uriHandler.openUri(SOURCE_CODE_URL) } @@ -160,7 +170,7 @@ private fun AppIdentityCard(versionName: String) { verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( - text = "GitHub Store", + text = stringResource(Res.string.tweaks_app_info_app_name), style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold, ), @@ -175,7 +185,7 @@ private fun AppIdentityCard(versionName: String) { ) Spacer(Modifier.height(4.dp)) Text( - text = "Cross-platform app store for GitHub, Codeberg, and Forgejo releases.", + text = stringResource(Res.string.tweaks_app_info_tagline), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt index d50d5c091..8007bc889 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/PasteProxyUrlSheet.kt @@ -19,9 +19,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url_cta +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url_error +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url_placeholder +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url_title import zed.rainxch.tweaks.presentation.model.ProxyType data class PastedProxy( @@ -88,6 +95,7 @@ fun PasteProxyUrlSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var input by remember { mutableStateOf("") } var error by remember { mutableStateOf(null) } + val parseErrorMessage = stringResource(Res.string.tweaks_connection_paste_url_error) ModalBottomSheet( onDismissRequest = onDismiss, @@ -101,14 +109,14 @@ fun PasteProxyUrlSheet( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = "Paste proxy URL", + text = stringResource(Res.string.tweaks_connection_paste_url_title), style = MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, ) Text( - text = "Paste a full proxy URL and we'll fill the form for you.", + text = stringResource(Res.string.tweaks_connection_paste_url_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -118,7 +126,7 @@ fun PasteProxyUrlSheet( input = it error = null }, - label = "scheme://user:pass@host:port", + label = stringResource(Res.string.tweaks_connection_paste_url_placeholder), isError = error != null, supportingText = error, modifier = Modifier.fillMaxWidth(), @@ -128,12 +136,12 @@ fun PasteProxyUrlSheet( onClick = { val parsed = parseProxyUrl(input) if (parsed == null) { - error = "Couldn't read that URL." + error = parseErrorMessage } else { onParsed(parsed) } }, - label = "Use this URL", + label = stringResource(Res.string.tweaks_connection_paste_url_cta), variant = GhsButtonVariant.Primary, modifier = Modifier.fillMaxWidth(), enabled = input.isNotBlank(), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index d6709fa02..b87afec9c 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -63,6 +63,28 @@ import zed.rainxch.githubstore.core.presentation.res.proxy_saved import zed.rainxch.githubstore.core.presentation.res.proxy_host import zed.rainxch.githubstore.core.presentation.res.proxy_test_success import zed.rainxch.githubstore.core.presentation.res.proxy_username +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_applies_to_all +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_custom +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_intro_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_intro_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_main_section +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_http +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_http_caption +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_no_proxy +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_socks +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_socks_caption +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_mode_system +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_overrides_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_overrides_section +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_paste_url +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_discovery_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_discovery_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_download_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_download_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_translation_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_scope_translation_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_test +import zed.rainxch.githubstore.core.presentation.res.tweaks_connection_use_main import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_connection import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksEvent @@ -170,7 +192,7 @@ private fun IntroCard() { ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "How the app reaches the internet", + text = stringResource(Res.string.tweaks_connection_intro_title), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.SemiBold, ), @@ -178,7 +200,7 @@ private fun IntroCard() { ) Spacer(Modifier.height(4.dp)) Text( - text = "Pick a connection mode below. Most people leave this on No proxy.", + text = stringResource(Res.string.tweaks_connection_intro_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -200,7 +222,7 @@ private fun MainConnectionCard( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Main connection", + text = stringResource(Res.string.tweaks_connection_main_section), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -226,14 +248,14 @@ private fun MainConnectionCard( ) Spacer(Modifier.height(4.dp)) Text( - text = "Applies to all traffic unless overridden below.", + text = stringResource(Res.string.tweaks_connection_applies_to_all), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Spacer(Modifier.height(8.dp)) GhsButton( onClick = onPasteUrl, - label = "Paste full URL", + label = stringResource(Res.string.tweaks_connection_paste_url), variant = GhsButtonVariant.Text, size = GhsButtonSize.Sm, leadingIcon = Icons.Outlined.ContentPaste, @@ -245,7 +267,7 @@ private fun MainConnectionCard( ) { GhsButton( onClick = { onAction(TweaksAction.OnMasterProxyTest) }, - label = "Test", + label = stringResource(Res.string.tweaks_connection_test), variant = GhsButtonVariant.Outline, leadingIcon = Icons.Default.NetworkCheck, enabled = !form.isTestInProgress, @@ -336,10 +358,16 @@ private fun ModePillSegment( onSelected: (ProxyType) -> Unit, ) { val items = listOf( - ProxyType.NONE to ("No proxy" to null), - ProxyType.SYSTEM to ("System" to null), - ProxyType.HTTP to ("HTTP/HTTPS" to "Most corporate proxies."), - ProxyType.SOCKS to ("SOCKS5" to "Tor, SSH tunnels."), + ProxyType.NONE to (stringResource(Res.string.tweaks_connection_mode_no_proxy) to null), + ProxyType.SYSTEM to (stringResource(Res.string.tweaks_connection_mode_system) to null), + ProxyType.HTTP to ( + stringResource(Res.string.tweaks_connection_mode_http) to + stringResource(Res.string.tweaks_connection_mode_http_caption) + ), + ProxyType.SOCKS to ( + stringResource(Res.string.tweaks_connection_mode_socks) to + stringResource(Res.string.tweaks_connection_mode_socks_caption) + ), ) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { @@ -404,7 +432,7 @@ private fun OverridesCard( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Per-scope overrides", + text = stringResource(Res.string.tweaks_connection_overrides_section), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -412,7 +440,7 @@ private fun OverridesCard( ) Spacer(Modifier.height(4.dp)) Text( - text = "Each scope uses the main connection by default. Choose 'Custom' to keep its own settings.", + text = stringResource(Res.string.tweaks_connection_overrides_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -443,9 +471,12 @@ private fun ScopeOverrideRow( onAction: (TweaksAction) -> Unit, ) { val (title, subtitle) = when (scope) { - ProxyScope.DISCOVERY -> "Search & metadata" to "GitHub API, search, repo details." - ProxyScope.DOWNLOAD -> "Downloads" to "APK and asset downloads." - ProxyScope.TRANSLATION -> "Translation" to "DeepL, Microsoft, LibreTranslate calls." + ProxyScope.DISCOVERY -> stringResource(Res.string.tweaks_connection_scope_discovery_title) to + stringResource(Res.string.tweaks_connection_scope_discovery_body) + ProxyScope.DOWNLOAD -> stringResource(Res.string.tweaks_connection_scope_download_title) to + stringResource(Res.string.tweaks_connection_scope_download_body) + ProxyScope.TRANSLATION -> stringResource(Res.string.tweaks_connection_scope_translation_title) to + stringResource(Res.string.tweaks_connection_scope_translation_body) } Surface( @@ -518,7 +549,7 @@ private fun ScopeOverrideRow( ) { GhsButton( onClick = { onAction(TweaksAction.OnProxyTest(scope)) }, - label = "Test", + label = stringResource(Res.string.tweaks_connection_test), variant = GhsButtonVariant.Outline, leadingIcon = Icons.Default.NetworkCheck, enabled = !scopeForm.isTestInProgress, @@ -570,12 +601,12 @@ private fun UseMainSegment( horizontalArrangement = Arrangement.spacedBy(3.dp), ) { SegmentChip( - label = "Use main", + label = stringResource(Res.string.tweaks_connection_use_main), selected = useMain, onClick = { onSelected(true) }, ) SegmentChip( - label = "Custom", + label = stringResource(Res.string.tweaks_connection_custom), selected = !useMain, onClick = { onSelected(false) }, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt index 9cbfc5446..fc8a990a8 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt @@ -42,6 +42,7 @@ import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.language_follow_system import zed.rainxch.githubstore.core.presentation.res.language_picker_title import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_language +import zed.rainxch.githubstore.core.presentation.res.tweaks_language_intro_body import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSearchField @@ -78,7 +79,7 @@ fun TweaksLanguageRoot( ) { item(key = "language_intro") { Text( - text = "The app restarts when you switch language.", + text = stringResource(Res.string.tweaks_language_intro_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp), diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt index 9a6c6394f..d07194b8f 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt @@ -23,8 +23,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_intro_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_intro_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_title import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold @@ -68,7 +73,7 @@ fun LicensesRoot( val uriHandler = LocalUriHandler.current TweaksSubScreenScaffold( - title = "Open source licenses", + title = stringResource(Res.string.tweaks_licenses_title), onNavigateBack = onNavigateBack, snackbarState = snackbarState, restartReasons = state.needsRestartReasons, @@ -85,13 +90,13 @@ fun LicensesRoot( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "GitHub Store stands on these libraries.", + text = stringResource(Res.string.tweaks_licenses_intro_title), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) Spacer(Modifier.height(4.dp)) Text( - text = "Tap any entry to open its project page.", + text = stringResource(Res.string.tweaks_licenses_intro_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt index 5e16bc804..69e7711b0 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -43,6 +43,28 @@ import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_privacy +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_browsing_history_section +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clear_viewed_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clear_viewed_dialog_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clear_viewed_dialog_confirm +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clear_viewed_dialog_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clear_viewed_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clipboard_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_clipboard_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hide_seen_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hide_seen_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hidden_repos_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hidden_repos_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_app_version +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_feature_usage +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_identifiers +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_repo_names +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_tokens +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_os +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_what_we_collect +import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_usage_data_section import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold @@ -77,8 +99,8 @@ fun TweaksPrivacyRoot( item(key = "clipboard_card") { ToggleCard( - title = "Detect repo links in clipboard", - subtitle = "When you copy a github.com or codeberg.org link, we'll prompt to open it.", + title = stringResource(Res.string.tweaks_privacy_clipboard_title), + subtitle = stringResource(Res.string.tweaks_privacy_clipboard_body), checked = state.autoDetectClipboardLinks, onCheckedChange = { viewModel.onAction(TweaksAction.OnAutoDetectClipboardToggled(it)) @@ -89,7 +111,7 @@ fun TweaksPrivacyRoot( item(key = "history_header") { Text( - text = "Browsing history", + text = stringResource(Res.string.tweaks_privacy_browsing_history_section), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -100,8 +122,8 @@ fun TweaksPrivacyRoot( item(key = "hide_seen_card") { ToggleCard( - title = "Hide repos I've already viewed", - subtitle = "Skip seen repos in feeds and search.", + title = stringResource(Res.string.tweaks_privacy_hide_seen_title), + subtitle = stringResource(Res.string.tweaks_privacy_hide_seen_body), checked = state.isHideSeenEnabled, onCheckedChange = { viewModel.onAction(TweaksAction.OnHideSeenToggled(it)) }, ) @@ -110,8 +132,8 @@ fun TweaksPrivacyRoot( item(key = "clear_history_row") { DestructiveRow( - title = "Clear viewed history", - subtitle = "Forget which repos you've already opened.", + title = stringResource(Res.string.tweaks_privacy_clear_viewed_title), + subtitle = stringResource(Res.string.tweaks_privacy_clear_viewed_body), onClick = { viewModel.onAction(TweaksAction.OnClearSeenHistoryRequest) }, ) Spacer(Modifier.height(8.dp)) @@ -120,8 +142,8 @@ fun TweaksPrivacyRoot( item(key = "hidden_repos_row") { DrillRow( icon = Icons.Outlined.VisibilityOff, - title = "Hidden repositories", - subtitle = "Repos you've muted from feeds and search.", + title = stringResource(Res.string.tweaks_privacy_hidden_repos_title), + subtitle = stringResource(Res.string.tweaks_privacy_hidden_repos_body), accent = TweaksAccents.Periwinkle, onClick = onNavigateToHiddenRepositories, ) @@ -130,9 +152,9 @@ fun TweaksPrivacyRoot( if (state.isClearSeenHistoryDialogVisible) { GhsConfirmDialog( - title = "Clear viewed history?", - body = "This won't unstar or unfavorite anything.", - confirmLabel = "Clear", + title = stringResource(Res.string.tweaks_privacy_clear_viewed_dialog_title), + body = stringResource(Res.string.tweaks_privacy_clear_viewed_dialog_body), + confirmLabel = stringResource(Res.string.tweaks_privacy_clear_viewed_dialog_confirm), destructive = true, onConfirm = { viewModel.onAction(TweaksAction.OnClearSeenHistoryConfirm) }, onDismiss = { viewModel.onAction(TweaksAction.OnClearSeenHistoryDismiss) }, @@ -155,7 +177,7 @@ private fun TelemetryCard( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Usage data", + text = stringResource(Res.string.tweaks_privacy_usage_data_section), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -173,14 +195,14 @@ private fun TelemetryCard( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Share anonymous usage data", + text = stringResource(Res.string.tweaks_privacy_telemetry_title), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, ) Text( - text = "Help us understand which features get used.", + text = stringResource(Res.string.tweaks_privacy_telemetry_body), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -203,7 +225,7 @@ private fun TelemetryCard( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - text = "What we collect", + text = stringResource(Res.string.tweaks_privacy_telemetry_what_we_collect), style = MaterialTheme.typography.labelLarge.copy( fontWeight = FontWeight.SemiBold, ), @@ -225,12 +247,12 @@ private fun TelemetryCard( verticalArrangement = Arrangement.spacedBy(2.dp), ) { listOf( - "App version.", - "OS and platform.", - "Feature usage counts.", - "No repo names.", - "No tokens.", - "No identifiers.", + stringResource(Res.string.tweaks_privacy_telemetry_collect_app_version), + stringResource(Res.string.tweaks_privacy_telemetry_collect_os), + stringResource(Res.string.tweaks_privacy_telemetry_collect_feature_usage), + stringResource(Res.string.tweaks_privacy_telemetry_collect_no_repo_names), + stringResource(Res.string.tweaks_privacy_telemetry_collect_no_tokens), + stringResource(Res.string.tweaks_privacy_telemetry_collect_no_identifiers), ).forEach { line -> Text( text = "• $line", diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt index 4838ac125..00023bbe8 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -43,7 +43,15 @@ import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.custom_forges_count +import zed.rainxch.githubstore.core.presentation.res.custom_forges_entry_label +import zed.rainxch.githubstore.core.presentation.res.remove_search_history_item import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_sources +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_add_a_host +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_added_hosts_section +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_github_mirror_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_intro_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_intro_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_mirror_default import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.CustomForgesDialog @@ -76,7 +84,7 @@ fun TweaksSourcesRoot( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = "Where the app looks for repositories", + text = stringResource(Res.string.tweaks_sources_intro_title), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.SemiBold, ), @@ -84,7 +92,7 @@ fun TweaksSourcesRoot( ) Spacer(Modifier.height(4.dp)) Text( - text = "The app searches GitHub by default. Route through a regional mirror or add custom Forgejo / Gitea hosts.", + text = stringResource(Res.string.tweaks_sources_intro_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -96,8 +104,8 @@ fun TweaksSourcesRoot( item(key = "mirror_row") { DrillRow( icon = Icons.Outlined.NetworkCheck, - title = "GitHub mirror", - subtitle = "Default (github.com)", + title = stringResource(Res.string.tweaks_sources_github_mirror_title), + subtitle = stringResource(Res.string.tweaks_sources_mirror_default), accent = TweaksAccents.Sky, onClick = onNavigateToMirrorPicker, ) @@ -108,9 +116,9 @@ fun TweaksSourcesRoot( val count = state.customForgeHosts.size DrillRow( icon = Icons.Outlined.Dns, - title = "Custom forges", + title = stringResource(Res.string.custom_forges_entry_label), subtitle = if (count == 0) { - "Add a Forgejo or Gitea host" + stringResource(Res.string.tweaks_sources_add_a_host) } else { pluralStringResource(Res.plurals.custom_forges_count, count, count) }, @@ -123,7 +131,7 @@ fun TweaksSourcesRoot( item(key = "forges_subheader") { Spacer(Modifier.height(16.dp)) Text( - text = "Added hosts", + text = stringResource(Res.string.tweaks_sources_added_hosts_section), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.SemiBold, ), @@ -260,7 +268,7 @@ private fun ForgeHostRow( ) { Icon( imageVector = Icons.Outlined.DeleteOutline, - contentDescription = "Remove", + contentDescription = stringResource(Res.string.remove_search_history_item), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp), ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt index 8efdff694..db138c0bd 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/storage/TweaksStorageRoot.kt @@ -42,6 +42,11 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.downloads_cleared import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_storage +import zed.rainxch.githubstore.core.presentation.res.tweaks_storage_downloaded_apks_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_storage_downloaded_apks_clear +import zed.rainxch.githubstore.core.presentation.res.tweaks_storage_downloaded_apks_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_storage_empty_size +import zed.rainxch.githubstore.core.presentation.res.tweaks_storage_using_label import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksEvent import zed.rainxch.tweaks.presentation.TweaksViewModel @@ -104,8 +109,9 @@ private fun DownloadsCard( cacheSize: String, onClearClick: () -> Unit, ) { - val sizeDisplay = cacheSize.ifBlank { "0 B" } - val isEmpty = sizeDisplay == "0 B" + val emptySize = stringResource(Res.string.tweaks_storage_empty_size) + val sizeDisplay = cacheSize.ifBlank { emptySize } + val isEmpty = sizeDisplay == emptySize Surface( modifier = Modifier.fillMaxWidth(), @@ -140,20 +146,20 @@ private fun DownloadsCard( verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( - text = "Downloaded APKs", + text = stringResource(Res.string.tweaks_storage_downloaded_apks_title), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, ) Text( - text = "We keep installers around so updates resume fast.", + text = stringResource(Res.string.tweaks_storage_downloaded_apks_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Spacer(Modifier.height(4.dp)) Text( - text = "Using: $sizeDisplay", + text = stringResource(Res.string.tweaks_storage_using_label, sizeDisplay), style = MaterialTheme.typography.labelMedium.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.SemiBold, @@ -164,7 +170,7 @@ private fun DownloadsCard( GhsButton( onClick = onClearClick, - label = "Clear", + label = stringResource(Res.string.tweaks_storage_downloaded_apks_clear), variant = GhsButtonVariant.Destructive, enabled = !isEmpty, leadingIcon = Icons.Outlined.DeleteOutline, From 0df88a90dc4fe0ce04827c2efefab2b3c75c7464 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 13:43:05 +0500 Subject: [PATCH 120/172] chore(p17): retire deprecated fraunces and jetbrainsMono refs --- .../githubstore/app/navigation/BottomNavigation.kt | 4 ++-- .../githubstore/app/navigation/DesktopDrawer.kt | 6 +++--- .../githubstore/app/onboarding/OnboardingScreen.kt | 10 +++++----- .../presentation/components/cards/WaxSealTrustCard.kt | 4 ++-- .../core/presentation/vocabulary/LicensePosture.kt | 4 ++-- .../details/presentation/components/ApkInspectSheet.kt | 1 + 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt index 98041e71e..15a284999 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.theme.GithubStoreTheme -import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.theme.geist import zed.rainxch.core.presentation.vocabulary.CookieShape import zed.rainxch.core.presentation.vocabulary.VersionStack @@ -155,7 +155,7 @@ private fun CookieTabItem( color = cs.primary, style = MaterialTheme.typography.labelSmall.copy( - fontFamily = fraunces, + fontFamily = geist, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, ), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt index 6fc4d73ea..ceca3726a 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/DesktopDrawer.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.theme.geist import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.vocabulary.CookieShape import zed.rainxch.core.presentation.vocabulary.VersionStack @@ -70,7 +70,7 @@ fun DesktopDrawer( color = Color.White, style = MaterialTheme.typography.titleMedium.copy( - fontFamily = fraunces, + fontFamily = geist, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, fontWeight = FontWeight.Bold, fontSize = 15.sp, @@ -82,7 +82,7 @@ fun DesktopDrawer( color = cs.onSurface, style = MaterialTheme.typography.titleMedium.copy( - fontFamily = fraunces, + fontFamily = geist, fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, fontWeight = FontWeight.SemiBold, ), diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index ff3a1dd51..fcc9f6058 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -45,7 +45,7 @@ import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant -import zed.rainxch.core.presentation.theme.fraunces +import zed.rainxch.core.presentation.theme.geist import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.primaryColor @@ -147,7 +147,7 @@ private fun StepPalette( text = "Pick your palette", style = MaterialTheme.typography.displaySmall.copy( - fontFamily = fraunces, + fontFamily = geist, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, @@ -271,7 +271,7 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { Text( text = "G", color = MaterialTheme.colorScheme.onPrimary, - fontFamily = fraunces, + fontFamily = geist, fontWeight = FontWeight.Bold, fontSize = 48.sp, ) @@ -280,7 +280,7 @@ private fun StepSignIn(onAction: (OnboardingAction) -> Unit) { text = "Sign in with GitHub", style = MaterialTheme.typography.headlineSmall.copy( - fontFamily = fraunces, + fontFamily = geist, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, @@ -313,7 +313,7 @@ private fun StepPermissions(controller: OnboardingPermissionsController) { text = "Two quick prompts", style = MaterialTheme.typography.headlineSmall.copy( - fontFamily = fraunces, + fontFamily = geist, fontWeight = FontWeight.SemiBold, ), color = MaterialTheme.colorScheme.onSurface, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt index 9e18a2f23..c10bb5320 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/cards/WaxSealTrustCard.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import zed.rainxch.core.presentation.theme.jetbrainsMono +import zed.rainxch.core.presentation.theme.geistMono import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.vocabulary.WaxSeal import zed.rainxch.core.presentation.vocabulary.WaxSealState @@ -55,7 +55,7 @@ fun WaxSealTrustCard( ) Text( text = fingerprintDetail, - fontFamily = jetbrainsMono, + fontFamily = geistMono, color = cs.onSurfaceVariant, style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp), ) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt index 5a0f3e4d6..f35e2652e 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/vocabulary/LicensePosture.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import zed.rainxch.core.presentation.theme.jetbrainsMono +import zed.rainxch.core.presentation.theme.geistMono import zed.rainxch.core.presentation.theme.tokens.Tokens @Composable @@ -26,7 +26,7 @@ fun LicensePosture( val heavy = spdx != null && spdx in Tokens.Licenses.copyleft val ink = MaterialTheme.colorScheme.onSurface val bg = MaterialTheme.colorScheme.background - val mono = jetbrainsMono + val mono = geistMono Box( modifier = modifier .size(sizeDp.dp) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt index 4ac66a7f5..b1f43addf 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt @@ -649,6 +649,7 @@ private fun protectionStyle(level: ProtectionLevel): Pair { @OptIn(ExperimentalFoundationApi::class) @Composable private fun Modifier.copyableOnLongPress(value: String): Modifier { + @Suppress("DEPRECATION") val clipboard = LocalClipboardManager.current val haptic = LocalHapticFeedback.current return this.combinedClickable( From c95279b82e5bbcdaff03c736fe623baf4ee66c74 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 15:55:17 +0500 Subject: [PATCH 121/172] chore(i18n): drop stale strings across English and 12 locales --- .../composeResources/values-ar/strings-ar.xml | 144 --------------- .../composeResources/values-bn/strings-bn.xml | 142 --------------- .../composeResources/values-es/strings-es.xml | 135 -------------- .../composeResources/values-fr/strings-fr.xml | 135 -------------- .../composeResources/values-hi/strings-hi.xml | 142 --------------- .../composeResources/values-it/strings-it.xml | 141 --------------- .../composeResources/values-ja/strings-ja.xml | 135 -------------- .../composeResources/values-ko/strings-ko.xml | 141 --------------- .../composeResources/values-pl/strings-pl.xml | 135 -------------- .../composeResources/values-ru/strings-ru.xml | 135 -------------- .../composeResources/values-tr/strings-tr.xml | 141 --------------- .../values-zh-rCN/strings-zh-rCN.xml | 135 -------------- .../composeResources/values/strings.xml | 168 ------------------ .../presentation/components/RepositoryCard.kt | 1 - .../components/whatsnew/WhatsNewSheet.kt | 1 - .../zed/rainxch/apps/presentation/AppsRoot.kt | 1 - .../language/TweaksLanguageRoot.kt | 1 - 17 files changed, 1833 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 23d3015dd..bfdd64a65 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -3,8 +3,6 @@ GitHub Store - - التطبيقات المثبتة العودة التحقق من التحديثات @@ -12,7 +10,6 @@ تعذر تشغيل %1$s فشل فتح %1$s فشل تحديث %1$s: %2$s - فشل التحديث فشل تحديث الكل: %1$s تم تحديث جميع التطبيقات بنجاح لا توجد تحديثات متاحة @@ -61,12 +58,9 @@ تسجيل الدخول بـ GitHub - تم الإلغاء خطأ غير معروف - اللغة: - اكتشف المستودعات ابحث عن مستودع، وصف… @@ -115,54 +109,29 @@ الملف الشخصي - - المظهر - اللغة - تجاوز لغة واجهة التطبيق. - لغة التطبيق - يغير القوائم والأزرار والرسائل في جميع أنحاء التطبيق. لا يغير المحتوى القادم من GitHub. اتباع النظام أعد التشغيل لتطبيق اللغة الجديدة. إعادة التشغيل - الشبكة - حول لون السمة سمة AMOLED السوداء خلفية سوداء نقية للوضع الداكن - اللون المحدد: %1$s - - الإصدار - المساعدة والدعم تسجيل الخروج - - نوع الوكيل - بدون - النظام - HTTP - SOCKS المضيف المنفذ اسم المستخدم (اختياري) كلمة المرور (اختياري) حفظ الوكيل تم حفظ إعدادات الوكيل - يستخدم إعدادات الوكيل الخاصة بجهازك - يجب أن يكون المنفذ بين 1–65535 - اتصال مباشر، بدون وكيل فشل حفظ إعدادات الوكيل المضيف مطلوب أدخل اسم مضيف أو عنوان IP صالحًا منفذ الوكيل غير صالح - إظهار كلمة المرور - إخفاء كلمة المرور - اختبار - جارٍ الاختبار… الاتصال ناجح (%1$d مللي ثانية) تعذر تحليل المضيف. تحقق من عنوان الوكيل. تعذر الوصول إلى خادم الوكيل. @@ -170,17 +139,9 @@ يلزم التحقق من الوكيل. استجابة غير متوقعة: HTTP %1$d فشل اختبار الاتصال - يمكن لكل فئة استخدام البروكسي الخاص بها. قم بتكوينها بشكل مستقل. - الاكتشاف (GitHub API) - الصفحة الرئيسية والبحث وتفاصيل المستودع وفحوصات التحديث - التنزيلات - تنزيلات APK والتحديثات التلقائية - الترجمة - خدمة ترجمة README تم تسجيل الخروج بنجاح، جارٍ إعادة التوجيه... - تم مسح ذاكرة التخزين المؤقت بنجاح تحذير! @@ -198,8 +159,6 @@ إلغاء التنزيل عرض خيارات التثبيت - - خطأ في تحميل التفاصيل إعادة المحاولة لا يوجد وصف. لا توجد ملاحظات إصدار. @@ -208,7 +167,6 @@ حول هذا التطبيق سجلات التثبيت - المؤلف ما الجديد @@ -216,8 +174,6 @@ تحديث متاح غير متاح تثبيت الأحدث - إعادة التثبيت - تحديث التطبيق جارٍ التنزيل @@ -257,14 +213,9 @@ لا توجد نتائج أخرى على GitHub فشل الجلب من GitHub. حاول مرة أخرى. - - بواسطة %1$s - • المثبت: %1$s متوافق مع المعمارية التحديث إلى %1$s - - فشل تحميل التفاصيل تم حفظ المثبت في مجلد التنزيلات @@ -292,30 +243,13 @@ خطأ: %1$s - نوع الملف .%1$s غير مدعوم - لم يتم العثور على الملف المنزّل - الرائج - إصدار ساخن - الأكثر شعبية - الخصوصية - الوسائط - الإنتاجية - الشبكة - أدوات المطور جارٍ البحث عن مستودعات... - جارٍ تحميل المزيد... - لا مزيد من المستودعات إعادة المحاولة فشل تحميل المستودعات - عرض التفاصيل - تم التحديث للتو - تم التحديث منذ %1$d ساعة - تم التحديث أمس - تم التحديث منذ %1$d يوم تم التحديث في %1$s تم تجاوز حد الطلبات @@ -364,8 +298,6 @@ تجاهل فشلت مزامنة المستودعات المميزة بنجمة - الملف الشخصي للمطور - فتح الملف الشخصي للمطور فشل تحميل المستودعات فشل تحميل الملف الشخصي @@ -378,7 +310,6 @@ البحث في المستودعات… مسح البحث - الكل مع إصدارات المثبتة المفضلة @@ -390,7 +321,6 @@ مستودع - مستودعات عرض %1$d من %2$d مستودع @@ -398,8 +328,6 @@ لا توجد مستودعات مثبتة لا توجد مستودعات مفضلة - - تم التحديث %1$s يحتوي على إصدار منذ %1$d سنة @@ -437,17 +365,11 @@ آخر فحص: %1$s - لم يتم الفحص مطلقاً الآن منذ %1$d دقيقة منذ %1$d ساعة جارٍ التحقق من التحديثات… - - تتبع هذا التطبيق - تمت إضافة التطبيق إلى قائمة التتبع - فشل تتبع التطبيق: %1$s - التطبيق قيد التتبع بالفعل تسجيل الدخول إلى GitHub @@ -467,8 +389,6 @@ سيؤدي هذا إلى مسح جلستك المحلية والبيانات المخزنة مؤقتاً. لإلغاء الوصول بالكامل، قم بزيارة إعدادات GitHub > التطبيقات. - - ينتهي الرمز خلال %1$s انتهت صلاحية رمز الجهاز. يرجى محاولة تسجيل الدخول مرة أخرى للحصول على رمز جديد. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى. @@ -485,28 +405,17 @@ فشل مشاركة الرابط تم نسخ الرابط إلى الحافظة - - ترجمة - جارٍ الترجمة… - عرض الأصلي - تُرجم إلى %1$s ترجمة إلى… البحث عن لغة - تغيير اللغة فشلت الترجمة. يرجى المحاولة مرة أخرى. فتح رابط GitHub تم اكتشاف رابط GitHub في الحافظة - الكشف التلقائي عن روابط الحافظة - الكشف التلقائي عن روابط GitHub من الحافظة عند فتح البحث الروابط المكتشفة فتح في التطبيق لم يتم العثور على رابط GitHub في الحافظة - التخزين - مسح ذاكرة التخزين المؤقت - الحجم الحالي: مسح @@ -523,8 +432,6 @@ - - التثبيت الافتراضي نافذة التثبيت القياسية للنظام Shizuku @@ -541,7 +448,6 @@ تحديث التطبيقات تلقائيًا تنزيل التحديثات وتثبيتها تلقائيًا في الخلفية عبر Shizuku - التحديثات فترة التحقق من التحديثات عدد مرات التحقق من تحديثات التطبيق في الخلفية التحقق التلقائي من التحديثات @@ -565,18 +471,13 @@ جارٍ التحقق… ربط وتتبع التحقق من آخر إصدار… - تنزيل APK للتحقق… التحقق من مفتاح التوقيع… عدم تطابق اسم الحزمة: ملف APK هو %1$s، لكن التطبيق المحدد هو %2$s عدم تطابق مفتاح التوقيع: ملف APK في هذا المستودع موقّع من مطور مختلف اختر المثبّت اختر ملف APK للتحقق من مطابقته للتطبيق المثبت - فشل التنزيل تصدير استيراد - استيراد التطبيقات - الصق ملف JSON المُصدَّر لاستعادة التطبيقات المتتبعة - الصق JSON المُصدَّر هنا… قناة البيتا الافتراضية تشمل التطبيقات التي تتعقبها حديثاً إصدارات البيتا افتراضياً. التطبيقات المتعقَّبة مسبقاً تحتفظ بإعدادها الخاص (بدّله من شاشة التفاصيل). إلغاء تثبيت التطبيق؟ @@ -589,9 +490,6 @@ تم ربط %1$s بـ %2$s/%3$s فشل التصدير: %1$s فشل الاستيراد: %1$s - تم استيراد %1$d تطبيقات - ، %1$d تم تخطيها - ، %1$d فشلت تغيّر مفتاح التوقيع تغيّرت شهادة توقيع هذا التطبيق منذ تثبيته لأول مرة.\n\nقد يعني هذا أن المطور غيّر مفتاح التوقيع، أو أن الملف قد تم التلاعب به.\n\nالمتوقع: %1$s\nالمستلم: %2$s التثبيت على أي حال @@ -604,8 +502,6 @@ ملفات متعددة متاحة تتوفر عدة ملفات قابلة للتثبيت لهذا الإصدار. يرجى مراجعة القائمة واختيار الملف المناسب لجهازك. معلومات - إعادة المحاولة - اكتشاف تلقائي: %1$s اختر اللغة عدم تطابق الحزمة: ملف APK هو %1$s، لكن التطبيق المثبت هو %2$s. تم حظر التحديث. عدم تطابق مفتاح التوقيع: تم توقيع التحديث بواسطة مطور مختلف. تم حظر التحديث. @@ -618,21 +514,13 @@ واسع واسع جدًا - إخفاء المستودعات المشاهَدة - إخفاء المستودعات التي شاهدتها بالفعل من خلاصات الاكتشاف - مسح سجل المشاهدة - إعادة تعيين جميع المستودعات المشاهَدة لتظهر مجدداً في الخلاصات تم مسح سجل المشاهدة تمت المشاهدة هذا المستودع لك إخفاء المستودع - تم إخفاء المستودع - تراجع تمت مشاهدته مؤخرًا المستودعات التي قمت بزيارتها - الحزم التي تم تنزيلها - ملفات APK والمثبتات من إصدارات GitHub حذف الكل حذف جميع التنزيلات؟ سيؤدي هذا إلى حذف جميع ملفات APK والمثبتات نهائيًا (%1$s). يمكنك إعادة تنزيلها في أي وقت. @@ -643,9 +531,7 @@ مسح الكل إزالة - تعديلات تعديلات - إصدارات تجريبية عامل تصفية الأصول @@ -705,8 +591,6 @@ جاهز للتثبيت تثبيت (جاهز) - - الترجمة اختر الخدمة المستخدمة لترجمة README. خدمة الترجمة يعمل Google عالميًا دون تهيئة. يعمل Youdao من الصين القارية ولكنه يتطلب بيانات اعتماد API من بوابة مطوري Youdao. @@ -749,8 +633,6 @@ ghp_… أو github_pat_… تسجيل الدخول إلغاء - عرض الرمز - إخفاء الرمز يرجى لصق رمز. هذا لا يبدو كرمز GitHub. تبدأ الرموز بـ ghp_ أو github_pat_. هذا الرمز غير صالح أو تم إلغاؤه. @@ -767,13 +649,11 @@ اختر قناة الإصدار اضغط للتبديل بين الإصدارات المستقرة وإصدارات البيتا لهذا التطبيق. حسناً - تبديل الإصدارات التجريبية لهذا التطبيق التبديل إلى الإصدار المستقر %1$s لا يوجد إصدار مستقر منذ %1$d أشهر لا يوجد إصدار مستقر منذ %1$d أيام إصدارات تجريبية نشطة لكن المشروع لم يُصدر بناءً مستقراً منذ فترة. قد لا تتقارب الإصدارات التجريبية إلى إصدار مستقر. ما الذي تغيّر منذ %1$s - — %1$s — استيراد التطبيقات المثبّتة @@ -796,7 +676,6 @@ المزيد من التطابقات إخفاء التطابقات مثبّت عبر %1$s - نعتقد أن هذا هو %1$s · %2$d اضغط للبحث عن مستودع توسيع لرؤية تطابقات أخرى طي البطاقة @@ -884,7 +763,6 @@ إلغاء الربط بهذا المستودع - المزيد من الخيارات إلغاء ربط هذا التطبيق؟ سنتوقف عن تتبع %1$s كمثبّت من هذا المستودع. يبقى التطبيق على جهازك — يُزال الرابط فقط. إلغاء الربط @@ -904,8 +782,6 @@ تعذر التحديث. حاول مرة أخرى لاحقاً. فشل التحديث. حاول مرة أخرى. - - إرسال ملاحظات إرسال ملاحظات إغلاق الفئة @@ -940,16 +816,12 @@ منصّات مخصّصة - أضف مضيفات Forgejo / Gitea ليتعرّف عليها GHS - %1$d مُضافة منصّات مخصّصة أدخل اسم مضيف Forgejo أو Gitea (مثل git.example.com). سيقبل GHS روابط هذه المضيفات في نموذج الربط اليدوي. إضافة لا توجد منصّات مخصّصة بعد. تم - - مرآة التنزيل مرآة التنزيل تُستخدم لتنزيل أصول الإصدار. تتجه مكالمات GitHub API دائماً مباشرة. يجب على معظم المستخدمين إبقاء هذا على GitHub المباشر. رسمي @@ -965,7 +837,6 @@ يجب أن يحتوي القالب على {url} مرة واحدة بالضبط حفظ اختبار المحدد - جارٍ الاختبار… تم الوصول في %1$dms أرجعت المرآة %1$d انتهت المهلة بعد 5 ثوانٍ @@ -973,12 +844,6 @@ فشل: %1$s كل المرايا معطّلة؟ يمكنك استضافة مرآتك الخاصة في 5 دقائق — راجع الوثائق. %1$s لم يعد متاحاً، تم التبديل إلى GitHub المباشر. - عدم تطابق المجموع الاختباري — قد يكون الملف قد تم التلاعب به - هل تجرب مرآة أسرع؟ - يُفضّل بعض المستخدمين على الشبكات البطيئة استخدام وكيل مجتمعي. - اختر واحدة - ربما لاحقاً - لا تسأل مجدداً إضافة من المميزة بنجمة @@ -989,13 +854,7 @@ الأمان الحالة استطلاعات - أغلق بعد الاطلاع - %1$d ي لا شيء لمشاركته الآن. - %1$d س - الآن - آخر تحديث منذ %1$s - %1$d د لا يمكن تعطيله إظهار العناصر من فتح إعدادات الكتم @@ -1052,7 +911,6 @@ مطوي موسّع محدَّث - تحديثات متاحة إسقاط المثبِّت المحفوظ لـ %1$s وإزالة الصف من قائمة تطبيقاتك؟ سيتم حذف ملف APK الذي تم تنزيله. إسقاط التثبيت المعلّق؟ منح الإذن @@ -1138,7 +996,6 @@ جديد ما الجديد في %1$s ما الجديد - سجل التغييرات بالإنجليزية. الترجمات مرحَّب بها عبر Issues. الإصدار %1$s · %2$s @@ -1176,7 +1033,6 @@ Windows macOS Linux - منصة أخرى — يفتح في المتصفح للحفظ والنقل جهازك للنقل أبقِ أندرويد مفتوحًا diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 3af1e866d..3dcec83ee 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -3,8 +3,6 @@ GitHub Store - - ইনস্টল করা অ্যাপসমূহ পেছনে যান আপডেট পরীক্ষা করুন @@ -12,7 +10,6 @@ %1$s চালু করা যায়নি %1$s খুলতে ব্যর্থ %1$s আপডেট করতে ব্যর্থ: %2$s - আপডেট ব্যর্থ সব আপডেট ব্যর্থ হয়েছে: %1$s সব অ্যাপ সফলভাবে আপডেট হয়েছে কোনো আপডেট পাওয়া যায়নি @@ -61,12 +58,9 @@ GitHub দিয়ে সাইন ইন করুন - বাতিল করা হয়েছে অজানা ত্রুটি - ভাষা: - রিপোজিটরি আবিষ্কার করুন রিপো, বিবরণ খুঁজুন… @@ -115,34 +109,21 @@ প্রোফাইল - - চেহারা - ভাষা - অ্যাপের UI ভাষা ওভাররাইড করুন। - অ্যাপের ভাষা - সম্পূর্ণ অ্যাপের মেনু, বোতাম এবং বার্তা পরিবর্তন করে। GitHub থেকে আসা বিষয়বস্তু পরিবর্তন করে না। সিস্টেম অনুসরণ করুন নতুন ভাষা প্রয়োগ করতে পুনরায় চালু করুন। পুনরায় চালু করুন - সম্পর্কে - নেটওয়ার্ক থিমের রঙ AMOLED কালো থিম অন্ধকার মোডের জন্য সম্পূর্ণ কালো ব্যাকগ্রাউন্ড - নির্বাচিত রঙ: %1$s - - সংস্করণ - সহায়তা ও সাপোর্ট লগআউট সফলভাবে লগআউট হয়েছে, রিডাইরেক্ট করা হচ্ছে... - ক্যাশ সফলভাবে পরিষ্কার করা হয়েছে সতর্কতা! @@ -160,8 +141,6 @@ ডাউনলোড বাতিল করুন ইনস্টল অপশন দেখান - - বিস্তারিত লোড করতে ত্রুটি দেখা গেছে পুনরায় চেষ্টা করুন কোনো বিবরণ দেওয়া হয়নি। কোনো রিলিজ নোট নেই। @@ -170,7 +149,6 @@ এই অ্যাপ বৃত্তান্ত ইনস্টল লগ - লেখক নতুন কী আছে @@ -178,8 +156,6 @@ আপডেট উপলব্ধ উপলব্ধ নয় সর্বশেষটি ইনস্টল করুন - পুনরায় ইনস্টল করুন - অ্যাপ আপডেট করুন ডাউনলোড হচ্ছে @@ -208,14 +184,9 @@ GitHub-এ আর ফলাফল নেই GitHub থেকে আনতে ব্যর্থ। আবার চেষ্টা করুন। - - %1$s দ্বারা - • ইনস্টল করা: %1$s আর্কিটেকচার উপযোগী %1$s -এ আপডেট করুন - - বিস্তারিত লোড করতে ব্যর্থ ইনস্টলারটি Downloads ফোল্ডারে সংরক্ষিত হয়েছে @@ -241,30 +212,13 @@ ত্রুটি: %1$s - .%1$s অ্যাসেট টাইপ সমর্থিত নয় - ডাউনলোড করা ফাইল পাওয়া যায়নি - ট্রেন্ডিং - হট রিলিজ - সবচেয়ে জনপ্রিয় - গোপনীয়তা - মিডিয়া - উৎপাদনশীলতা - নেটওয়ার্ক - ডেভ টুলস রিপোজিটরি খোঁজা হচ্ছে... - আরও লোড হচ্ছে... - আর কোনো রিপোজিটরি নেই পুনরায় চেষ্টা রিপোজিটরি লোড করতে ব্যর্থ - বিস্তারিত দেখুন - এইমাত্র আপডেট হয়েছে - %1$d ঘণ্টা আগে আপডেট হয়েছে - গতকাল আপডেট হয়েছে - %1$d দিন আগে আপডেট হয়েছে %1$s তারিখে আপডেট হয়েছে রেট লিমিট অতিক্রম করেছে @@ -313,8 +267,6 @@ বন্ধ করুন স্টার করা রিপোজিটরি সিঙ্ক করতে ব্যর্থ হয়েছে - ডেভেলপার প্রোফাইল - ডেভেলপার প্রোফাইল খুলুন রিপোজিটরি লোড করতে ব্যর্থ @@ -328,7 +280,6 @@ রিপোজিটরি খুঁজুন… অনুসন্ধান মুছুন - সব রিলিজ সহ ইনস্টল করা পছন্দ @@ -340,7 +291,6 @@ রিপোজিটরি - রিপোজিটরি %2$d টির মধ্যে %1$d টি রিপোজিটরি দেখানো হচ্ছে @@ -348,8 +298,6 @@ কোনো রিপোজিটরি ইনস্টল করা হয়নি কোনো পছন্দের রিপোজিটরি নেই - - %1$s আপডেট করা হয়েছে রিলিজ আছে @@ -402,35 +350,21 @@ সর্বশেষ পরীক্ষা: %1$s - কখনো পরীক্ষা করা হয়নি এইমাত্র %1$d মিনিট আগে %1$d ঘণ্টা আগে আপডেট পরীক্ষা করা হচ্ছে… - - প্রক্সি ধরন - নেই - সিস্টেম - HTTP - SOCKS হোস্ট পোর্ট ব্যবহারকারীর নাম (ঐচ্ছিক) পাসওয়ার্ড (ঐচ্ছিক) প্রক্সি সংরক্ষণ প্রক্সি সেটিংস সংরক্ষিত হয়েছে - আপনার ডিভাইসের প্রক্সি সেটিংস ব্যবহার করে - পোর্ট ১–৬৫৫৩৫ এর মধ্যে হতে হবে - সরাসরি সংযোগ, কোনো প্রক্সি নেই প্রক্সি সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে প্রক্সি হোস্ট প্রয়োজন একটি বৈধ হোস্টনাম বা IP ঠিকানা লিখুন অবৈধ প্রক্সি পোর্ট - পাসওয়ার্ড দেখান - পাসওয়ার্ড লুকান - পরীক্ষা - পরীক্ষা চলছে… সংযোগ ঠিক আছে (%1$d ms) হোস্ট সমাধান করা যায়নি। প্রক্সি ঠিকানা যাচাই করুন। প্রক্সি সার্ভারে পৌঁছানো যায়নি। @@ -438,20 +372,8 @@ প্রক্সি প্রমাণীকরণ প্রয়োজন। অপ্রত্যাশিত প্রতিক্রিয়া: HTTP %1$d সংযোগ পরীক্ষা ব্যর্থ - প্রতিটি বিভাগ তার নিজস্ব প্রক্সি ব্যবহার করতে পারে। সেগুলি স্বাধীনভাবে কনফিগার করুন। - আবিষ্কার (GitHub API) - হোম, অনুসন্ধান, রেপো বিবরণ এবং আপডেট চেক - ডাউনলোড - APK ডাউনলোড এবং স্বয়ংক্রিয় আপডেট - অনুবাদ - README অনুবাদ পরিষেবা - - এই অ্যাপ ট্র্যাক করুন - অ্যাপ ট্র্যাকিং তালিকায় যোগ করা হয়েছে - অ্যাপ ট্র্যাক করতে ব্যর্থ: %1$s - অ্যাপটি ইতিমধ্যে ট্র্যাক করা হচ্ছে GitHub-এ সাইন ইন করুন @@ -468,7 +390,6 @@ আবার সাইন ইন করুন অতিথি হিসেবে চালিয়ে যান এটি আপনার স্থানীয় সেশন এবং ক্যাশ ডেটা মুছে ফেলবে। সম্পূর্ণরূপে অ্যাক্সেস প্রত্যাহার করতে, GitHub Settings > Applications এ যান। - কোডের মেয়াদ শেষ হবে %1$s এ ডিভাইস কোডের মেয়াদ শেষ হয়ে গেছে। একটি নতুন কোড পেতে অনুগ্রহ করে আবার সাইন ইন করার চেষ্টা করুন। অনুগ্রহ করে আপনার ইন্টারনেট সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন। @@ -485,27 +406,16 @@ লিংক শেয়ার করতে ব্যর্থ হয়েছে লিংক ক্লিপবোর্ডে কপি করা হয়েছে - - অনুবাদ করুন - অনুবাদ হচ্ছে… - মূল দেখান - %1$s এ অনুবাদিত অনুবাদ করুন… ভাষা খুঁজুন - ভাষা পরিবর্তন করুন অনুবাদ ব্যর্থ হয়েছে। আবার চেষ্টা করুন। GitHub লিংক খুলুন ক্লিপবোর্ডে GitHub লিংক পাওয়া গেছে - ক্লিপবোর্ড লিংক স্বয়ংক্রিয় সনাক্তকরণ - অনুসন্ধান খোলার সময় স্বয়ংক্রিয়ভাবে ক্লিপবোর্ড থেকে GitHub লিংক সনাক্ত করুন সনাক্তকৃত লিংক অ্যাপে খুলুন ক্লিপবোর্ডে কোনো GitHub লিংক পাওয়া যায়নি - স্টোরেজ - ক্যাশে পরিষ্কার করুন - বর্তমান আকার: পরিষ্কার করুন @@ -522,8 +432,6 @@ - - ইনস্টলেশন ডিফল্ট স্ট্যান্ডার্ড সিস্টেম ইনস্টল ডায়ালগ Shizuku @@ -540,7 +448,6 @@ স্বয়ংক্রিয়ভাবে অ্যাপ আপডেট করুন Shizuku এর মাধ্যমে ব্যাকগ্রাউন্ডে স্বয়ংক্রিয়ভাবে আপডেট ডাউনলোড এবং ইনস্টল করুন - আপডেট আপডেট চেক করার ব্যবধান ব্যাকগ্রাউন্ডে কতক্ষণ পর পর অ্যাপ আপডেট খোঁজা হবে ব্যাকগ্রাউন্ড আপডেট চেক @@ -564,18 +471,13 @@ যাচাই হচ্ছে… লিঙ্ক এবং ট্র্যাক করুন সর্বশেষ রিলিজ পরীক্ষা হচ্ছে… - যাচাইয়ের জন্য APK ডাউনলোড হচ্ছে… সাইনিং কী যাচাই হচ্ছে… প্যাকেজ নাম মেলেনি: APK হলো %1$s, কিন্তু নির্বাচিত অ্যাপ হলো %2$s সাইনিং কী মেলেনি: এই রিপোজিটরির APK একজন ভিন্ন ডেভেলপার দ্বারা স্বাক্ষরিত ইনস্টলার নির্বাচন করুন আপনার ইনস্টল করা অ্যাপের সাথে যাচাই করতে APK নির্বাচন করুন - ডাউনলোড ব্যর্থ রপ্তানি আমদানি - অ্যাপ আমদানি করুন - ট্র্যাক করা অ্যাপ পুনরুদ্ধার করতে রপ্তানি করা JSON পেস্ট করুন - রপ্তানি করা JSON এখানে পেস্ট করুন… ডিফল্ট বেটা চ্যানেল নতুন ট্র্যাক করা অ্যাপ ডিফল্টভাবে বেটা বিল্ড অন্তর্ভুক্ত করবে। ইতিমধ্যে ট্র্যাক করা অ্যাপ তাদের নিজস্ব সেটিং রাখবে (অ্যাপের বিস্তারিত স্ক্রিনে পরিবর্তন করুন)। অ্যাপ আনইনস্টল করবেন? @@ -588,9 +490,6 @@ %1$s %2$s/%3$s এর সাথে লিঙ্ক করা হয়েছে রপ্তানি ব্যর্থ: %1$s আমদানি ব্যর্থ: %1$s - %1$d অ্যাপ আমদানি করা হয়েছে - , %1$d বাদ দেওয়া হয়েছে - , %1$d ব্যর্থ সাইনিং কী পরিবর্তিত হয়েছে এই অ্যাপের সাইনিং সার্টিফিকেট প্রথম ইনস্টলের পর থেকে পরিবর্তিত হয়েছে।\n\nএর অর্থ হতে পারে ডেভেলপার তাদের সাইনিং কী পরিবর্তন করেছে, অথবা বাইনারি পরিবর্তন করা হয়েছে।\n\nপ্রত্যাশিত: %1$s\nপ্রাপ্ত: %2$s যাই হোক ইনস্টল করুন @@ -603,8 +502,6 @@ একাধিক সম্পদ উপলব্ধ এই রিলিজের জন্য একাধিক ইনস্টলযোগ্য ফাইল উপলব্ধ। তালিকা পর্যালোচনা করুন এবং আপনার ডিভাইসের জন্য উপযুক্তটি নির্বাচন করুন। তথ্য - পুনরায় চেষ্টা - স্বয়ংক্রিয়ভাবে শনাক্ত: %1$s ভাষা নির্বাচন করুন প্যাকেজ অমিল: APK হলো %1$s, কিন্তু ইনস্টল করা অ্যাপ হলো %2$s। আপডেট ব্লক করা হয়েছে। সাইনিং কী অমিল: আপডেটটি একজন ভিন্ন ডেভেলপার দ্বারা সাইন করা হয়েছে। আপডেট ব্লক করা হয়েছে। @@ -617,21 +514,13 @@ প্রশস্ত অতিরিক্ত প্রশস্ত - দেখা রিপোজিটরি লুকান - আপনি ইতিমধ্যে দেখেছেন এমন রিপোজিটরি আবিষ্কার ফিড থেকে লুকান - দেখার ইতিহাস মুছুন - সমস্ত দেখা রিপোজিটরি রিসেট করুন যাতে সেগুলো ফিডে আবার দেখা যায় দেখার ইতিহাস মুছে ফেলা হয়েছে দেখা হয়েছে এই রিপো আপনার রিপোজিটরি লুকান - রিপোজিটরি লুকানো হয়েছে - পূর্বাবস্থা সাম্প্রতিক দেখা আপনি যেসব রিপোজিটরি দেখেছেন - ডাউনলোড করা প্যাকেজসমূহ - GitHub রিলিজ থেকে APK এবং ইনস্টলার সব মুছুন সব ডাউনলোড মুছবেন? এটি সব APK এবং ইনস্টলার স্থায়ীভাবে মুছে ফেলবে (%1$s)। আপনি যেকোনো সময় আবার ডাউনলোড করতে পারবেন। @@ -642,9 +531,7 @@ সব মুছুন সরান - টুইকস টুইকস - প্রি-রিলিজ অ্যাসেট ফিল্টার @@ -704,8 +591,6 @@ ইনস্টলের জন্য প্রস্তুত ইনস্টল (প্রস্তুত) - - অনুবাদ README অনুবাদের জন্য ব্যবহৃত পরিষেবা নির্বাচন করুন। অনুবাদ পরিষেবা Google বিশ্বব্যাপী কনফিগারেশন ছাড়াই কাজ করে। Youdao মূল ভূখণ্ডের চীন থেকে কাজ করে কিন্তু Youdao ডেভেলপার পোর্টাল থেকে API শংসাপত্র প্রয়োজন। @@ -748,8 +633,6 @@ ghp_… বা github_pat_… সাইন ইন বাতিল - টোকেন দেখান - টোকেন লুকান অনুগ্রহ করে একটি টোকেন পেস্ট করুন। এটি GitHub টোকেন বলে মনে হচ্ছে না। টোকেন ghp_ বা github_pat_ দিয়ে শুরু হয়। এই টোকেনটি অবৈধ অথবা বাতিল করা হয়েছে। @@ -767,13 +650,11 @@ আপনার রিলিজ চ্যানেল বাছুন এই অ্যাপের স্থিতিশীল রিলিজ এবং বেটা বিল্ডের মধ্যে স্যুইচ করতে আলতো চাপুন। বুঝেছি - এই অ্যাপের জন্য বেটা রিলিজ টগল করুন স্থিতিশীল %1$s-এ যান %1$d মাসে কোনো স্থিতিশীল রিলিজ নেই %1$d দিনে কোনো স্থিতিশীল রিলিজ নেই সক্রিয় প্রি-রিলিজ রয়েছে কিন্তু প্রকল্পটি কিছুদিন ধরে স্থিতিশীল বিল্ড প্রকাশ করেনি। বেটা স্থিতিশীল রিলিজে পরিণত নাও হতে পারে। %1$s থেকে কী পরিবর্তন হয়েছে - — %1$s — ইনস্টল করা অ্যাপ আমদানি করুন @@ -792,7 +673,6 @@ আরও মিল মিল লুকান %1$s দিয়ে ইনস্টল করা - আমরা মনে করি এটি %1$s · %2$d রিপো খুঁজতে ট্যাপ করুন অন্য মিল দেখতে প্রসারিত করুন কার্ড সংকুচিত করুন @@ -860,7 +740,6 @@ এই রিপো থেকে আনলিঙ্ক করুন - আরও বিকল্প এই অ্যাপ আনলিঙ্ক করবেন? এই রিপো থেকে ইনস্টল হিসেবে %1$s ট্র্যাক করা বন্ধ করব। অ্যাপটি আপনার ডিভাইসে থাকবে — শুধু লিঙ্ক সরানো হবে। আনলিঙ্ক করুন @@ -880,8 +759,6 @@ রিফ্রেশ করা যায়নি। শীঘ্রই আবার চেষ্টা করুন। রিফ্রেশ ব্যর্থ হয়েছে। আবার চেষ্টা করুন। - - ফিডব্যাক পাঠান ফিডব্যাক পাঠান বন্ধ করুন বিভাগ @@ -917,15 +794,12 @@ কাস্টম ফোর্জ - GHS যেসব Forgejo / Gitea হোস্ট চিনবে যোগ করুন - %1$d যোগ করা হয়েছে কাস্টম ফোর্জ Forgejo বা Gitea ইনস্ট্যান্সের হোস্টনেম দিন (যেমন git.example.com)। ম্যানুয়াল লিঙ্ক শিটে GHS এই হোস্টের URL গ্রহণ করবে। যোগ এখনো কোনো কাস্টম ফোর্জ নেই। সম্পন্ন - ডাউনলোড মিরর ডাউনলোড মিরর রিলিজ অ্যাসেট ডাউনলোড করতে ব্যবহৃত। GitHub API কল সবসময় সরাসরি যায়। বেশিরভাগ ব্যবহারকারীর Direct GitHub-এ রাখা উচিত। অফিসিয়াল @@ -941,7 +815,6 @@ টেমপ্লেটে {url} ঠিক একবার থাকতে হবে সংরক্ষণ করুন নির্বাচিত পরীক্ষা করুন - পরীক্ষা চলছে… %1$dms-এ পৌঁছানো হয়েছে মিরর %1$d ফেরত দিয়েছে 5 সেকেন্ড পরে টাইমআউট @@ -949,12 +822,6 @@ ব্যর্থ: %1$s সব মিরর নষ্ট? ৫ মিনিটে নিজের মিরর হোস্ট করুন — ডক্স দেখুন। %1$s আর পাওয়া যাচ্ছে না, Direct GitHub-এ স্যুইচ করা হয়েছে। - চেকসাম মিলছে না — ফাইলটি হয়তো পরিবর্তন করা হয়েছে - আরও দ্রুত মিরর চেষ্টা করবেন? - ধীর নেটওয়ার্কে কিছু ব্যবহারকারী কমিউনিটি প্রক্সিতে ভালো ফলাফল পান। - একটি বেছে নিন - পরে হয়তো - আর জিজ্ঞেস করবেন না স্টার করা থেকে যোগ করুন @@ -965,13 +832,7 @@ সুরক্ষা অবস্থা জরিপ - দেখার পর বন্ধ করুন - %1$d দিন এই মুহূর্তে শেয়ার করার মতো কিছু নেই। - %1$d ঘ - এইমাত্র - %1$s আগে সর্বশেষ রিফ্রেশ হয়েছে - %1$d মি নিষ্ক্রিয় করা যাবে না আইটেম দেখান মিউট সেটিংস খুলুন @@ -1028,7 +889,6 @@ সংকুচিত প্রসারিত আপ টু ডেট - আপডেট পাওয়া যাচ্ছে %1$s এর পার্ক করা ইনস্টলার বাদ দিয়ে আপনার অ্যাপের তালিকা থেকে সারিটি সরাবেন? ডাউনলোড করা APK মুছে ফেলা হবে। মুলতুবি ইনস্টল বাদ দেবেন? অনুমতি দিন @@ -1114,7 +974,6 @@ নতুন %1$s এ নতুন কী আছে নতুন কী আছে - চেঞ্জলগ ইংরেজিতে। Issues-এর মাধ্যমে অনুবাদ স্বাগত। সংস্করণ %1$s · %2$s @@ -1152,7 +1011,6 @@ Windows macOS Linux - অন্য প্ল্যাটফর্ম — ট্রান্সফারের জন্য সংরক্ষণে ব্রাউজারে খুলবে আপনার ডিভাইস ট্রান্সফারের জন্য Android-কে উন্মুক্ত রাখুন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index c69ed96e0..f06b137ab 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1,14 +1,12 @@ GitHub Store - Aplicaciones instaladas Volver Buscar actualizaciones No se puede iniciar %1$s No se pudo abrir %1$s Error al actualizar %1$s: %2$s - Error en la actualización Error al actualizar todo: %1$s Todas las aplicaciones se actualizaron correctamente No hay actualizaciones disponibles @@ -48,12 +46,9 @@ Iniciar sesión con GitHub - Cancelado Error desconocido - Idioma: - Descubrir repositorios Buscar repositorio, descripción… Filtrar por lenguaje @@ -98,29 +93,18 @@ Perfil - APARIENCIA - IDIOMA - Reemplaza el idioma de la interfaz. - Idioma de la aplicación - Cambia los menús, botones y mensajes en toda la aplicación. No cambia el contenido que viene de GitHub. Seguir el sistema Reinicia para aplicar el nuevo idioma. Reiniciar - ACERCA DE - RED Color del tema Tema negro AMOLED Fondo negro puro para modo oscuro - Color seleccionado: %1$s - Versión - Ayuda y soporte Cerrar sesión Sesión cerrada correctamente, redirigiendo… - Caché borrada con éxito ¡Advertencia! ¿Estás seguro de que deseas cerrar sesión? @@ -130,16 +114,13 @@ Cream Plum - Error al cargar detalles Acerca de esta app Registros de instalación - Autor Novedades Instalado Actualización disponible Instalar última versión - Reinstalar Descargando Actualizando @@ -158,12 +139,9 @@ No hay más resultados en GitHub Error al buscar en GitHub. Inténtalo de nuevo. - por %1$s - • Instalado: %1$s Arquitectura compatible Actualizar a %1$s - No se pudieron cargar los detalles El instalador se guardó en Descargas Descarga iniciada @@ -187,30 +165,13 @@ Usar una aplicación de terceros para instalar el APK Error: %1$s - Tipo de archivo .%1$s no compatible - Archivo descargado no encontrado - Tendencias - Lanzamiento en caliente - Los más populares - Privacidad - Multimedia - Productividad - Red - Herramientas de desarrollo Buscando repositorios... - Cargando... - No hay más repositorios Reintentar No se pudieron cargar los repositorios - Ver detalles - actualizado ahora mismo - actualizado hace %1$d h - actualizado ayer - actualizado hace %1$d días actualizado el %1$s Límite de solicitudes excedido @@ -259,8 +220,6 @@ Cerrar No se pudieron sincronizar los repositorios destacados - Perfil del desarrollador - Abrir perfil de desarrollador Error al cargar repositorios @@ -274,7 +233,6 @@ Buscar repositorios… Borrar búsqueda - Todos Con lanzamientos Instalados Favoritos @@ -286,7 +244,6 @@ repositorio - repositorios Mostrando %1$d de %2$d repositorios @@ -294,8 +251,6 @@ No hay repositorios instalados No hay repositorios favoritos - - Actualizado hace %1$s Tiene lanzamiento @@ -343,7 +298,6 @@ No disponible - Actualizar app Instalación pendiente @@ -367,35 +321,21 @@ Última comprobación: %1$s - Nunca comprobado justo ahora hace %1$d min hace %1$d h Comprobando actualizaciones… - - Tipo de proxy - Ninguno - Sistema - HTTP - SOCKS Host Puerto Nombre de usuario (opcional) Contraseña (opcional) Guardar proxy Configuración de proxy guardada - Usa la configuración de proxy del dispositivo - El puerto debe ser 1–65535 - Conexión directa, sin proxy No se pudieron guardar los ajustes del proxy Se requiere el host del proxy Introduce un nombre de host o dirección IP válida Puerto de proxy no válido - Mostrar contraseña - Ocultar contraseña - Probar - Probando… Conexión correcta (%1$d ms) No se pudo resolver el host. Verifica la dirección del proxy. No se pudo conectar con el servidor proxy. @@ -403,20 +343,8 @@ Se requiere autenticación del proxy. Respuesta inesperada: HTTP %1$d La prueba de conexión falló - Cada categoría puede usar su propio proxy. Configúralos de forma independiente. - Descubrimiento (API de GitHub) - Inicio, búsqueda, detalles del repo y comprobación de actualizaciones - Descargas - Descargas de APK y actualizaciones automáticas - Traducción - Servicio de traducción de README - - Rastrear esta app - App añadida a la lista de seguimiento - Error al rastrear la app: %1$s - La app ya está siendo rastreada Iniciar sesión en GitHub @@ -433,7 +361,6 @@ Iniciar sesión de nuevo Continuar como invitado Esto borrará tu sesión local y los datos en caché. Para revocar el acceso completamente, visita GitHub Settings > Applications. - El código expira en %1$s El código del dispositivo ha expirado. Intenta iniciar sesión de nuevo para obtener un nuevo código. Revisa tu conexión a internet e intenta de nuevo. @@ -450,27 +377,16 @@ No se pudo compartir el enlace Enlace copiado al portapapeles - - Traducir - Traduciendo… - Mostrar original - Traducido a %1$s Traducir a… Buscar idioma - Cambiar idioma Error de traducción. Inténtalo de nuevo. Abrir enlace de GitHub Enlace de GitHub detectado en el portapapeles - Detectar enlaces del portapapeles - Detectar automáticamente enlaces de GitHub del portapapeles al abrir la búsqueda Enlaces detectados Abrir en la app No se encontró enlace de GitHub en el portapapeles - Almacenamiento - Borrar caché - Tamaño actual: Borrar @@ -483,8 +399,6 @@ - - Instalación Predeterminado Diálogo de instalación estándar del sistema Shizuku @@ -501,7 +415,6 @@ Actualizar apps automáticamente Descargar e instalar actualizaciones en segundo plano a través de Shizuku - Actualizaciones Intervalo de verificación Con qué frecuencia buscar actualizaciones en segundo plano Comprobación de actualizaciones en segundo plano @@ -525,18 +438,13 @@ Validando… Vincular y seguir Comprobando último lanzamiento… - Descargando APK para verificación… Verificando clave de firma… Nombre de paquete no coincide: el APK es %1$s, pero la app seleccionada es %2$s Clave de firma no coincide: el APK de este repositorio fue firmado por un desarrollador diferente Seleccionar instalador Elige el APK para verificar contra tu app instalada - Error en la descarga Exportar Importar - Importar apps - Pega el JSON exportado para restaurar tus apps rastreadas - Pega el JSON exportado aquí… Canal beta predeterminado Las apps recién seguidas incluyen compilaciones beta por defecto. Las ya seguidas conservan su propio canal (cámbialo en la pantalla de Detalles de la app). ¿Desinstalar app? @@ -549,9 +457,6 @@ %1$s vinculada a %2$s/%3$s Error en la exportación: %1$s Error en la importación: %1$s - %1$d apps importadas - , %1$d omitidas - , %1$d fallidas Clave de firma cambiada El certificado de firma de esta app ha cambiado desde su primera instalación.\n\nEsto podría significar que el desarrollador rotó su clave de firma, o el binario pudo haber sido manipulado.\n\nEsperado: %1$s\nRecibido: %2$s Instalar de todos modos @@ -564,8 +469,6 @@ Múltiples recursos disponibles Hay varios archivos instalables disponibles para este lanzamiento. Revisa la lista y selecciona el que se ajuste a tu dispositivo. Información - Reintentar - Detectado automáticamente: %1$s Seleccionar idioma Paquete no coincide: el APK es %1$s, pero la aplicación instalada es %2$s. Actualización bloqueada. Clave de firma no coincide: la actualización fue firmada por un desarrollador diferente. Actualización bloqueada. @@ -578,21 +481,13 @@ Ancho Extra ancho - Ocultar repositorios vistos - Ocultar repositorios que ya has visto de las fuentes de descubrimiento - Borrar historial de vistos - Restablecer todos los repositorios vistos para que aparezcan de nuevo en las fuentes Historial de vistos borrado Visto Eres dueño de este repo Ocultar repositorio - Repositorio ocultado - Deshacer Vistos recientemente Repositorios que has visitado - Paquetes descargados - APKs e instaladores desde lanzamientos de GitHub Eliminar todo ¿Eliminar todas las descargas? Esto eliminará permanentemente todos los APKs e instaladores (%1$s). Puedes volver a descargarlos en cualquier momento. @@ -603,9 +498,7 @@ Borrar todo Eliminar - Ajustes Ajustes - Pre-lanzamientos Filtro de assets @@ -665,8 +558,6 @@ Listo para instalar Instalar (listo) - - Traducción Elige el servicio usado para traducir los README. Servicio de traducción Google funciona globalmente sin configuración. Youdao funciona desde China continental, pero requiere credenciales API del portal de desarrolladores de Youdao. @@ -709,8 +600,6 @@ ghp_… o github_pat_… Iniciar sesión Cancelar - Mostrar token - Ocultar token Pega un token. Esto no parece un token de GitHub. Los tokens empiezan con ghp_ o github_pat_. Ese token no es válido o ha sido revocado. @@ -731,13 +620,11 @@ Elige tu canal de lanzamiento Toca para alternar entre lanzamientos estables y compilaciones beta de esta app. Entendido - Alternar versiones beta de esta aplicación Cambiar a %1$s estable Sin versión estable en %1$d meses Sin versión estable en %1$d días Hay versiones previas activas, pero el proyecto no ha lanzado una versión estable en un tiempo. Las betas podrían no derivar en una versión estable. Novedades desde %1$s - — %1$s — Desvincular de este repositorio - Más opciones ¿Desvincular esta aplicación? Dejaremos de rastrear %1$s como instalada desde este repositorio. La aplicación permanece en tu dispositivo — solo se elimina el vínculo. Desvincular @@ -851,7 +736,6 @@ - Enviar comentarios Enviar comentarios Cerrar Categoría @@ -889,15 +773,12 @@ ═══════════════════════════════════════════════════════════════ --> Forjas personalizadas - Añade hosts Forgejo / Gitea que quieras que GHS reconozca - %1$d añadidas Forjas personalizadas Introduce el hostname de una instancia Forgejo o Gitea (por ejemplo git.example.com). GHS aceptará URLs de estos hosts en el formulario de vinculación manual. Añadir Aún no hay forjas personalizadas. Hecho - Espejo de descarga Espejo de descarga Se usa para descargar archivos de versiones. Las llamadas a la API de GitHub siempre van directas. La mayoría de los usuarios debería dejarlo en GitHub directo. Oficial @@ -913,7 +794,6 @@ La plantilla debe contener {url} exactamente una vez Guardar Probar seleccionado - Probando… Alcanzado en %1$dms El espejo devolvió %1$d Tiempo de espera agotado tras 5s @@ -921,12 +801,6 @@ Error: %1$s ¿Todos los espejos caídos? Puedes alojar el tuyo en 5 minutos — consulta la documentación. %1$s ya no está disponible; cambiado a GitHub directo. - Error de suma de verificación — el archivo puede haber sido manipulado - ¿Probar un espejo más rápido? - Algunos usuarios en redes lentas tienen mejor suerte con un proxy de la comunidad. - Elegir uno - Quizás más tarde - No preguntar de nuevo Añadir desde destacados @@ -937,13 +811,7 @@ Seguridad Estado Encuestas - Cerrar tras confirmar - %1$d d Nada que compartir ahora mismo. - %1$d h - ahora mismo - Actualizado hace %1$s - %1$d min No se puede desactivar Mostrar elementos de Abrir ajustes de silencio @@ -1000,7 +868,6 @@ Contraído Expandido Al día - Actualizaciones disponibles ¿Descartar el instalador aparcado de %1$s y quitar la fila de tu lista de apps? El APK descargado se eliminará. ¿Descartar instalación pendiente? Conceder permiso @@ -1086,7 +953,6 @@ Nuevo Novedades en %1$s Novedades - El changelog está en inglés. Las traducciones son bienvenidas vía Issues. Versión %1$s · %2$s @@ -1124,7 +990,6 @@ Windows macOS Linux - Otra plataforma — se abre en el navegador para guardar y transferir Tu dispositivo Para transferir Mantén Android abierto diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 5bb81341b..f6cb3b716 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1,14 +1,12 @@ GitHub Store - Applications installées Retour Vérifier les mises à jour Impossible de lancer %1$s Impossible d’ouvrir %1$s Échec de la mise à jour de %1$s : %2$s - Échec de la mise à jour Échec de la mise à jour globale : %1$s Toutes les applications ont été mises à jour Aucune mise à jour disponible @@ -48,12 +46,9 @@ Se connecter avec GitHub - Annulé Erreur inconnue - Langue : - Découvrir des dépôts Rechercher dépôt, description… Filtrer par langage @@ -98,29 +93,18 @@ Profil - APPARENCE - LANGUE - Remplace la langue de l'interface. - Langue de l'application - Modifie les menus, boutons et messages dans toute l'application. Ne modifie pas le contenu provenant de GitHub. Suivre le système Redémarrez pour appliquer la nouvelle langue. Redémarrer - À PROPOS - RÉSEAU Couleur du thème Thème noir AMOLED Fond noir pur pour le mode sombre - Couleur sélectionnée : %1$s - Version - Aide et support Se déconnecter Déconnexion réussie, redirection… - Cache vidé avec succès Attention ! Voulez-vous vraiment vous déconnecter ? @@ -130,16 +114,13 @@ Cream Plum - Erreur de chargement À propos de cette application Journaux d’installation - Auteur Nouveautés Installé Mise à jour disponible Installer la dernière version - Réinstaller Téléchargement Mise à jour @@ -158,12 +139,9 @@ Plus de résultats sur GitHub Échec de la recherche GitHub. Réessayez. - par %1$s - • Installé : %1$s Architecture compatible Mettre à jour vers %1$s - Impossible de charger les détails L’installateur a été enregistré dans Téléchargements Téléchargement démarré @@ -187,30 +165,13 @@ Utiliser une application tierce pour installer l'APK Erreur : %1$s - Type de fichier .%1$s non pris en charge - Fichier téléchargé introuvable - Tendances - Sortie en salles - Les plus populaires - Confidentialité - Médias - Productivité - Réseau - Outils de développement Recherche de dépôts... - Chargement... - Plus aucun dépôt Réessayer Impossible de charger les dépôts - Voir les détails - mis à jour à l’instant - mis à jour il y a %1$d h - mis à jour hier - mis à jour il y a %1$d j mis à jour le %1$s Limite de requêtes dépassée @@ -259,8 +220,6 @@ Fermer Échec de la synchronisation des dépôts favoris - Profil du développeur - Ouvrir le profil du développeur Échec du chargement des dépôts @@ -274,7 +233,6 @@ Rechercher des dépôts… Effacer la recherche - Tous Avec versions Installés Favoris @@ -286,7 +244,6 @@ dépôt - dépôts Affichage de %1$d sur %2$d dépôts @@ -295,8 +252,6 @@ Aucun dépôt favori Signaler un problème - - Mis à jour %1$s A une version @@ -343,7 +298,6 @@ Non disponible - Mettre à jour Installation en attente @@ -367,35 +321,21 @@ Dernière vérification : %1$s - Jamais vérifié à l'instant il y a %1$d min il y a %1$d h Vérification des mises à jour… - - Type de proxy - Aucun - Système - HTTP - SOCKS Hôte Port Nom d'utilisateur (facultatif) Mot de passe (facultatif) Sauvegarder le Proxy Paramètres du proxy enregistrés - Utilise les paramètres proxy de l'appareil - Le port doit être entre 1 et 65535 - Connexion directe, pas de proxy Échec de l'enregistrement des paramètres du proxy L’hôte du proxy est requis Saisissez un nom d’hôte ou une adresse IP valide Port proxy invalide - Afficher le mot de passe - Masquer le mot de passe - Tester - Test en cours… Connexion OK (%1$d ms) Impossible de résoudre l'hôte. Vérifiez l'adresse du proxy. Impossible de joindre le serveur proxy. @@ -403,20 +343,8 @@ Authentification proxy requise. Réponse inattendue : HTTP %1$d Échec du test de connexion - Chaque catégorie peut utiliser son propre proxy. Configurez-les indépendamment. - Découverte (API GitHub) - Accueil, recherche, détails du dépôt et vérifications de mise à jour - Téléchargements - Téléchargements APK et mises à jour automatiques - Traduction - Service de traduction du README - - Suivre cette app - App ajoutée à la liste de suivi - Échec du suivi de l'app : %1$s - L'app est déjà suivie Se connecter à GitHub @@ -433,7 +361,6 @@ Se reconnecter Continuer en tant qu'invité Cela effacera votre session locale et les données en cache. Pour révoquer complètement l'accès, visitez GitHub Settings > Applications. - Le code expire dans %1$s Le code de l'appareil a expiré. Veuillez réessayer de vous connecter pour obtenir un nouveau code. Vérifiez votre connexion internet et réessayez. @@ -450,27 +377,16 @@ Échec du partage du lien Lien copié dans le presse-papiers - - Traduire - Traduction… - Afficher l'original - Traduit en %1$s Traduire en… Rechercher une langue - Changer de langue Échec de la traduction. Veuillez réessayer. Ouvrir le lien GitHub Lien GitHub détecté dans le presse-papiers - Détecter les liens du presse-papiers - Détecter automatiquement les liens GitHub du presse-papiers lors de l'ouverture de la recherche Liens détectés Ouvrir dans l'app Aucun lien GitHub trouvé dans le presse-papiers - Stockage - Vider le cache - Taille actuelle : Vider @@ -484,8 +400,6 @@ - - Installation Par défaut Boîte de dialogue d'installation système standard Shizuku @@ -502,7 +416,6 @@ Mise à jour automatique Télécharger et installer automatiquement les mises à jour en arrière-plan via Shizuku - Mises à jour Intervalle de vérification Fréquence de vérification des mises à jour en arrière-plan Vérification des mises à jour en arrière-plan @@ -526,18 +439,13 @@ Validation… Lier et suivre Vérification de la dernière version… - Téléchargement de l'APK pour vérification… Vérification de la clé de signature… Nom de paquet différent : l'APK est %1$s, mais l'app sélectionnée est %2$s Clé de signature différente : l'APK de ce dépôt a été signé par un autre développeur Sélectionner l'installateur Choisissez l'APK à vérifier avec votre app installée - Échec du téléchargement Exporter Importer - Importer des apps - Collez le JSON exporté pour restaurer vos apps suivies - Collez le JSON exporté ici… Canal bêta par défaut Les apps nouvellement suivies incluent les versions bêta par défaut. Les apps déjà suivies conservent leur propre paramètre (modifiable dans Détails de l'app). Désinstaller l'app ? @@ -550,9 +458,6 @@ %1$s liée à %2$s/%3$s Échec de l'exportation : %1$s Échec de l'importation : %1$s - %1$d apps importées - , %1$d ignorées - , %1$d échouées Clé de signature modifiée Le certificat de signature de cette app a changé depuis sa première installation.\n\nCela peut signifier que le développeur a changé sa clé de signature, ou que le binaire a été altéré.\n\nAttendu : %1$s\nReçu : %2$s Installer quand même @@ -565,8 +470,6 @@ Plusieurs ressources disponibles Plusieurs fichiers installables sont disponibles pour cette version. Veuillez examiner la liste et sélectionner celui qui correspond à votre appareil. Informations - Réessayer - Détection automatique : %1$s Sélectionner la langue Incompatibilité de paquet : l'APK est %1$s, mais l'application installée est %2$s. Mise à jour bloquée. Incompatibilité de clé de signature : la mise à jour a été signée par un développeur différent. Mise à jour bloquée. @@ -579,21 +482,13 @@ Large Très large - Masquer les dépôts consultés - Masquer les dépôts que vous avez déjà consultés des flux de découverte - Effacer l'historique des consultations - Réinitialiser tous les dépôts consultés pour qu'ils réapparaissent dans les flux Historique des consultations effacé Consulté Vous possédez ce dépôt Masquer le dépôt - Dépôt masqué - Annuler Récemment consultés Dépôts que vous avez visités - Paquets téléchargés - APK et installateurs depuis les versions GitHub Tout supprimer Supprimer tous les téléchargements ? Cela supprimera définitivement tous les APK et installateurs (%1$s). Vous pouvez les télécharger à nouveau à tout moment. @@ -604,9 +499,7 @@ Tout effacer Supprimer - Réglages Réglages - Pré-versions Filtre d'assets @@ -666,8 +559,6 @@ Prêt à installer Installer (prêt) - - Traduction Choisissez le service utilisé pour traduire les README. Service de traduction Google fonctionne partout sans configuration. Youdao fonctionne depuis la Chine continentale mais nécessite des identifiants API du portail développeur de Youdao. @@ -710,8 +601,6 @@ ghp_… ou github_pat_… Se connecter Annuler - Afficher le token - Masquer le token Collez un token. Cela ne ressemble pas à un token GitHub. Les tokens commencent par ghp_ ou github_pat_. Ce token est invalide ou a été révoqué. @@ -732,13 +621,11 @@ Choisissez votre canal de version Touchez pour basculer entre les versions stables et les bêtas pour cette app. Compris - Activer/désactiver les versions bêta de cette application Passer à %1$s stable Aucune version stable depuis %1$d mois Aucune version stable depuis %1$d jour(s) Des préversions actives existent, mais le projet n'a pas publié de version stable depuis un moment. Les bêtas pourraient ne pas aboutir à une version stable. Nouveautés depuis %1$s - — %1$s — Délier de ce dépôt - Plus d'options Délier cette application ? Nous cesserons de suivre %1$s comme installée depuis ce dépôt. L'application reste sur votre appareil — seul le lien est supprimé. Délier @@ -852,7 +737,6 @@ - Envoyer un commentaire Envoyer un commentaire Fermer Catégorie @@ -890,15 +774,12 @@ ═══════════════════════════════════════════════════════════════ --> Forges personnalisées - Ajoute des hôtes Forgejo / Gitea que tu veux que GHS reconnaisse - %1$d ajoutées Forges personnalisées Saisis le nom d'hôte d'une instance Forgejo ou Gitea (ex. git.example.com). GHS acceptera les URLs de ces hôtes dans le formulaire de liaison manuelle. Ajouter Aucune forge personnalisée pour l'instant. Terminé - Miroir de téléchargement Miroir de téléchargement Utilisé pour télécharger les fichiers de versions. Les appels à l'API GitHub passent toujours en direct. La plupart des utilisateurs devraient laisser ce paramètre sur GitHub direct. Officiel @@ -914,7 +795,6 @@ Le modèle doit contenir {url} exactement une fois Enregistrer Tester la sélection - Test en cours… Atteint en %1$dms Le miroir a renvoyé %1$d Délai dépassé après 5s @@ -922,12 +802,6 @@ Échec : %1$s Tous les miroirs hors ligne ? Vous pouvez héberger le vôtre en 5 minutes — consultez la documentation. %1$s n'est plus disponible, basculé vers GitHub direct. - Somme de contrôle incorrecte — le fichier a peut-être été altéré - Essayer un miroir plus rapide ? - Certains utilisateurs sur des réseaux lents ont plus de succès avec un proxy communautaire. - En choisir un - Peut-être plus tard - Ne plus demander Ajouter depuis les favoris @@ -938,13 +812,7 @@ Sécurité État Sondages - Fermer après prise en compte - %1$d j Rien à partager pour le moment. - %1$d h - à l'instant - Actualisé il y a %1$s - %1$d min Impossible à désactiver Afficher les éléments de Ouvrir les paramètres de silence @@ -1001,7 +869,6 @@ Réduite Développée À jour - Mises à jour disponibles Abandonner l'installeur en attente pour %1$s et retirer la ligne de votre liste d'apps ? L'APK téléchargé sera supprimé. Abandonner l'installation en attente ? Accorder l'autorisation @@ -1087,7 +954,6 @@ Nouveau Nouveautés de %1$s Nouveautés - Le changelog est en anglais. Traductions bienvenues via les Issues. Version %1$s · %2$s @@ -1125,7 +991,6 @@ Windows macOS Linux - Autre plateforme — s'ouvre dans le navigateur pour enregistrement et transfert Votre appareil Pour transfert Gardez Android ouvert diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 0bbdb7b13..cab664a3b 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -3,8 +3,6 @@ GitHub Store - - इंस्टॉल किए गए ऐप्स वापस जाएँ अपडेट के लिए जाँच करें @@ -12,7 +10,6 @@ %1$s को लॉन्च नहीं किया जा सकता %1$s को खोलने में विफल रहा %1$s अपडेट करने में विफल रहा: %2$s - अपडेट विफल सभी अपडेट विफल रहे: %1$s सभी ऐप्स सफलतापूर्वक अपडेट हो गए कोई अपडेट उपलब्ध नहीं @@ -61,12 +58,9 @@ GitHub से साइन इन करें - रद्द किया गया अज्ञात त्रुटि - भाषा: - रिपॉजिटरीज़ खोजें रेपो, विवरण खोजें… @@ -115,34 +109,21 @@ प्रोफ़ाइल - - उपस्थिति - भाषा - ऐप की UI भाषा को बदलें। - ऐप भाषा - पूरे ऐप में मेनू, बटन और संदेश बदलता है। GitHub से आने वाली सामग्री नहीं बदलती। सिस्टम का पालन करें नई भाषा लागू करने के लिए पुनरारंभ करें। पुनरारंभ करें - के बारे में - नेटवर्क थीम रंग AMOLED ब्लैक थीम डार्क मोड के लिए गहरा काला बैकग्राउंड - चुना हुआ रंग: %1$s - - संस्करण - सहायता & समर्थन लॉग आउट सफलतापूर्वक लॉग आउट हो गए, रीडायरेक्ट किया जा रहा है... - कैश सफलतापूर्वक साफ़ किया गया चेतावनी! @@ -160,8 +141,6 @@ डाउनलोड रद्द करें इंस्टॉल विकल्प दिखाएँ - - विवरण लोड करने में त्रुटि पुन: प्रयास करें कोई विवरण नहीं दिया गया है। कोई रिलीज़ नोट्स नहीं। @@ -170,7 +149,6 @@ इस ऐप के बारे में इंस्टॉल लॉग्स - लेखक नया क्या है @@ -178,8 +156,6 @@ उपलब्ध अपडेट उपलब्ध नहीं है नवीनतम स्थापित करें - पुनः इंस्टॉल करें - ऐप अपडेट करें डाउनलोड हो रहा है @@ -208,14 +184,9 @@ GitHub पर और परिणाम नहीं हैं GitHub से लाने में विफल। पुनः प्रयास करें। - - द्वारा %1$s - • स्थापित: %1$s आर्किटेक्चर संगत %1$s पर अपडेट करें - - विवरण लोड करने में विफल इंस्टॉलर को डाउनलोड फ़ोल्डर में सेव कर दिया गया है। @@ -241,20 +212,11 @@ गलती: %1$s - संपदा प्रकार .%1$s समर्थित नहीं - डाउनलोड की गई फ़ाइल नहीं मिली रिपॉजिटरी ढूंढी जा रही हैं... - और लोड हो रहा है... - अब और कोई रिपॉजिटरी नहीं पुन: प्रयास करें रिपॉजिटरी लोड करने में विफल रहा - विवरण देखें - अभी-अभी अपडेट किया गया - %1$d घंटे पहले अपडेट किया गया - कल अपडेट किया गया - %1$d दिन पहले अपडेट किया गया %1$s को अपडेट किया गया दर सीमा पार हो गई @@ -303,8 +265,6 @@ हटाएं तारांकित रिपॉजिटरी को सिंक करने में विफल रहा - डेवलपर प्रोफ़ाइल - डेवलपर प्रोफ़ाइल खोलें रिपॉजिटरी लोड करने में विफल रहा प्रोफ़ाइल लोड करने में विफल @@ -317,7 +277,6 @@ रिपॉजिटरी खोजें… खोज साफ़ करें - सभी रिलीज़ के साथ स्थापित पसंदीदा @@ -329,7 +288,6 @@ रिपॉजिटरी - रिपॉजिटरीज़ %2$d में से %1$d रिपॉजिटरी दिखाए जा रहे हैं @@ -337,8 +295,6 @@ कोई रिपॉजिटरी इंस्टॉल नहीं है कोई पसंदीदा रिपॉजिटरी नहीं - - %1$s पहले अपडेट किया गया रिलीज़ हो गया है %1$d साल पहले @@ -372,16 +328,7 @@ कोई संस्करण चयनित नहीं संस्करण - - रुझान - हॉट रिलीज़ - सबसे लोकप्रिय - गोपनीयता - मीडिया - उत्पादकता - नेटवर्क - डेव टूल्स इंस्टॉल लंबित @@ -401,35 +348,21 @@ अंतिम जाँच: %1$s - कभी जाँच नहीं की अभी %1$d मिनट पहले %1$d घंटे पहले अपडेट की जाँच हो रही है… - - प्रॉक्सी प्रकार - कोई नहीं - सिस्टम - HTTP - SOCKS होस्ट पोर्ट उपयोगकर्ता नाम (वैकल्पिक) पासवर्ड (वैकल्पिक) प्रॉक्सी सहेजें प्रॉक्सी सेटिंग्स सहेजी गईं - आपके डिवाइस की प्रॉक्सी सेटिंग का उपयोग करता है - पोर्ट 1–65535 के बीच होना चाहिए - सीधा कनेक्शन, कोई प्रॉक्सी नहीं प्रॉक्सी सेटिंग्स सहेजने में विफल प्रॉक्सी होस्ट आवश्यक है मान्य होस्टनाम या IP पता दर्ज करें अमान्य प्रॉक्सी पोर्ट - पासवर्ड दिखाएँ - पासवर्ड छुपाएँ - परीक्षण - परीक्षण हो रहा है… कनेक्शन ठीक है (%1$d ms) होस्ट हल नहीं हो सका। प्रॉक्सी पता जाँचें। प्रॉक्सी सर्वर तक नहीं पहुँचा जा सका। @@ -437,20 +370,8 @@ प्रॉक्सी प्रमाणीकरण आवश्यक है। अप्रत्याशित प्रतिक्रिया: HTTP %1$d कनेक्शन परीक्षण विफल - प्रत्येक श्रेणी अपना प्रॉक्सी उपयोग कर सकती है। उन्हें स्वतंत्र रूप से कॉन्फ़िगर करें। - खोज (GitHub API) - होम, खोज, रेपो विवरण और अपडेट जांच - डाउनलोड - APK डाउनलोड और स्वचालित अपडेट - अनुवाद - README अनुवाद सेवा - - इस ऐप को ट्रैक करें - ऐप ट्रैकिंग सूची में जोड़ा गया - ऐप ट्रैक करने में विफल: %1$s - ऐप पहले से ट्रैक हो रहा है GitHub में साइन इन करें @@ -467,7 +388,6 @@ फिर से साइन इन करें अतिथि के रूप में जारी रखें यह आपका स्थानीय सत्र और कैश डेटा साफ़ कर देगा। पूर्ण रूप से पहुँच रद्द करने के लिए, GitHub Settings > Applications पर जाएँ। - कोड %1$s में समाप्त होगा डिवाइस कोड की अवधि समाप्त हो गई है। नया कोड प्राप्त करने के लिए कृपया फिर से साइन इन करें। कृपया अपना इंटरनेट कनेक्शन जाँचें और पुनः प्रयास करें। @@ -484,27 +404,16 @@ लिंक साझा करने में विफल लिंक क्लिपबोर्ड में कॉपी किया गया - - अनुवाद करें - अनुवाद हो रहा है… - मूल दिखाएं - %1$s में अनुवादित अनुवाद करें… भाषा खोजें - भाषा बदलें अनुवाद विफल। कृपया पुनः प्रयास करें। GitHub लिंक खोलें क्लिपबोर्ड में GitHub लिंक मिला - क्लिपबोर्ड लिंक स्वतः पहचानें - खोज खोलते समय क्लिपबोर्ड से GitHub लिंक स्वचालित रूप से पहचानें पहचाने गए लिंक ऐप में खोलें क्लिपबोर्ड में कोई GitHub लिंक नहीं मिला - संग्रहण - कैश साफ़ करें - वर्तमान आकार: साफ़ करें @@ -521,8 +430,6 @@ - - इंस्टॉलेशन डिफ़ॉल्ट मानक सिस्टम इंस्टॉल डायलॉग Shizuku @@ -539,7 +446,6 @@ ऐप्स ऑटो-अपडेट करें Shizuku के माध्यम से पृष्ठभूमि में स्वचालित रूप से अपडेट डाउनलोड और इंस्टॉल करें - अपडेट अपडेट जाँच अंतराल पृष्ठभूमि में ऐप अपडेट कितनी बार जाँचें पृष्ठभूमि अपडेट जाँच @@ -563,18 +469,13 @@ सत्यापन हो रहा है… लिंक करें और ट्रैक करें नवीनतम रिलीज़ की जाँच हो रही है… - सत्यापन के लिए APK डाउनलोड हो रहा है… साइनिंग कुंजी सत्यापित हो रही है… पैकेज नाम मेल नहीं खाता: APK %1$s है, लेकिन चयनित ऐप %2$s है साइनिंग कुंजी मेल नहीं खाती: इस रिपॉजिटरी का APK किसी अन्य डेवलपर द्वारा हस्ताक्षरित है इंस्टॉलर चुनें अपने इंस्टॉल किए गए ऐप से मिलान करने के लिए APK चुनें - डाउनलोड विफल निर्यात आयात - ऐप्स आयात करें - अपने ट्रैक किए गए ऐप्स को पुनर्स्थापित करने के लिए निर्यात किया गया JSON पेस्ट करें - निर्यात किया गया JSON यहाँ पेस्ट करें… डिफ़ॉल्ट बीटा चैनल नए ट्रैक किए गए ऐप डिफ़ॉल्ट रूप से बीटा बिल्ड शामिल करते हैं। पहले से ट्रैक किए गए ऐप अपनी सेटिंग रखते हैं (ऐप के विवरण स्क्रीन से बदलें)। ऐप अनइंस्टॉल करें? @@ -587,9 +488,6 @@ %1$s को %2$s/%3$s से लिंक किया गया निर्यात विफल: %1$s आयात विफल: %1$s - %1$d ऐप्स आयात किए गए - , %1$d छोड़े गए - , %1$d विफल साइनिंग कुंजी बदल गई इस ऐप का साइनिंग प्रमाणपत्र पहली बार इंस्टॉल होने के बाद बदल गया है।\n\nइसका मतलब हो सकता है कि डेवलपर ने अपनी साइनिंग कुंजी बदल दी, या बाइनरी के साथ छेड़छाड़ की गई हो।\n\nअपेक्षित: %1$s\nप्राप्त: %2$s फिर भी इंस्टॉल करें @@ -602,8 +500,6 @@ कई संसाधन उपलब्ध इस रिलीज़ के लिए कई इंस्टॉल करने योग्य फ़ाइलें उपलब्ध हैं। कृपया सूची की समीक्षा करें और अपने डिवाइस के लिए उपयुक्त फ़ाइल चुनें। जानकारी - पुनः प्रयास - स्वतः पहचाना गया: %1$s भाषा चुनें पैकेज मेल नहीं खाता: APK %1$s है, लेकिन इंस्टॉल किया गया ऐप %2$s है। अपडेट ब्लॉक किया गया। साइनिंग कुंजी मेल नहीं खाती: अपडेट किसी अन्य डेवलपर द्वारा साइन किया गया था। अपडेट ब्लॉक किया गया। @@ -616,21 +512,13 @@ चौड़ा अतिरिक्त चौड़ा - देखे गए रिपॉजिटरी छुपाएँ - पहले से देखे गए रिपॉजिटरी को डिस्कवरी फ़ीड से छुपाएँ - देखने का इतिहास साफ़ करें - सभी देखे गए रिपॉजिटरी रीसेट करें ताकि वे फ़ीड में फिर से दिखें देखने का इतिहास साफ़ किया गया देखा गया यह रेपो आपका है रिपॉजिटरी छिपाएं - रिपॉजिटरी छिपाई गई - पूर्ववत् हाल ही में देखा गया वे रिपॉजिटरी जिन्हें आपने देखा है - डाउनलोड किए गए पैकेज - GitHub रिलीज़ से APK और इंस्टॉलर सभी हटाएँ सभी डाउनलोड हटाएँ? यह सभी APK और इंस्टॉलर को स्थायी रूप से हटा देगा (%1$s)। आप उन्हें कभी भी फिर से डाउनलोड कर सकते हैं। @@ -641,9 +529,7 @@ सभी साफ करें हटाएँ - ट्वीक्स ट्वीक्स - प्री-रिलीज़ एसेट फ़िल्टर @@ -703,8 +589,6 @@ इंस्टॉल के लिए तैयार इंस्टॉल (तैयार) - - अनुवाद README का अनुवाद करने के लिए उपयोग की जाने वाली सेवा चुनें। अनुवाद सेवा Google बिना कॉन्फ़िगरेशन के वैश्विक रूप से काम करता है। Youdao मुख्यभूमि चीन से काम करता है लेकिन Youdao डेवलपर पोर्टल से API क्रेडेंशियल्स की आवश्यकता है। @@ -747,8 +631,6 @@ ghp_… या github_pat_… साइन इन रद्द करें - टोकन दिखाएं - टोकन छुपाएं कृपया एक टोकन पेस्ट करें। यह GitHub टोकन नहीं लगता। टोकन ghp_ या github_pat_ से शुरू होते हैं। यह टोकन अमान्य है या निरस्त कर दिया गया है। @@ -770,13 +652,11 @@ अपना रिलीज़ चैनल चुनें इस ऐप के स्थिर रिलीज़ और बीटा बिल्ड के बीच स्विच करने के लिए टैप करें। समझ गया - इस ऐप के बीटा रिलीज़ टॉगल करें %1$s स्थिर पर स्विच करें %1$d महीनों में कोई स्थिर रिलीज़ नहीं %1$d दिनों में कोई स्थिर रिलीज़ नहीं सक्रिय प्री-रिलीज़ हैं, लेकिन प्रोजेक्ट ने काफी समय से कोई स्थिर बिल्ड नहीं दिया। बीटा स्थिर रिलीज़ में नहीं बदल सकते। %1$s के बाद क्या बदला - — %1$s — इस रिपो से अनलिंक करें - अधिक विकल्प इस ऐप को अनलिंक करें? हम %1$s को इस रिपो से इंस्टॉल के रूप में ट्रैक करना बंद कर देंगे। ऐप आपके डिवाइस पर रहेगा — केवल लिंक हटाया जाएगा। अनलिंक करें @@ -890,7 +768,6 @@ - फ़ीडबैक भेजें फ़ीडबैक भेजें बंद करें श्रेणी @@ -928,15 +805,12 @@ ═══════════════════════════════════════════════════════════════ --> कस्टम फोर्ज - जो Forgejo / Gitea होस्ट GHS पहचाने उन्हें जोड़ें - %1$d जोड़े गए कस्टम फोर्ज Forgejo या Gitea इंस्टेंस का होस्टनेम डालें (जैसे git.example.com)। GHS मैन्युअल लिंक शीट में इन होस्ट के URL स्वीकार करेगा। जोड़ें अभी कोई कस्टम फोर्ज नहीं। पूर्ण - डाउनलोड मिरर डाउनलोड मिरर रिलीज़ फ़ाइलें डाउनलोड करने के लिए उपयोग किया जाता है। GitHub API कॉल हमेशा सीधे जाते हैं। अधिकांश उपयोगकर्ताओं को इसे Direct GitHub पर छोड़ देना चाहिए। आधिकारिक @@ -952,7 +826,6 @@ टेम्पलेट में {url} ठीक एक बार होना चाहिए सहेजें चुने को टेस्ट करें - परीक्षण चल रहा है… %1$dms में पहुंचा मिरर ने %1$d लौटाया 5s बाद टाइमआउट @@ -960,12 +833,6 @@ विफल: %1$s सभी मिरर बंद हैं? आप 5 मिनट में अपना खुद का होस्ट कर सकते हैं — दस्तावेज़ देखें। %1$s अब उपलब्ध नहीं है, Direct GitHub पर स्विच किया गया। - चेकसम मेल नहीं — फ़ाइल से छेड़छाड़ हो सकती है - तेज़ मिरर आज़माएं? - धीमे नेटवर्क पर कुछ उपयोगकर्ताओं को कम्युनिटी प्रॉक्सी से बेहतर परिणाम मिलते हैं। - एक चुनें - शायद बाद में - दोबारा न पूछें तारांकित से जोड़ें @@ -976,13 +843,7 @@ सुरक्षा स्थिति सर्वेक्षण - पुष्टि के बाद बंद करें - %1$d दिन अभी साझा करने के लिए कुछ नहीं है। - %1$d घं - अभी-अभी - %1$s पहले रिफ़्रेश हुआ - %1$d मि बंद नहीं किया जा सकता आइटम दिखाएं म्यूट सेटिंग खोलें @@ -1039,7 +900,6 @@ संक्षिप्त विस्तृत अद्यतन - अपडेट उपलब्ध %1$s के लिए पार्क किए गए इंस्टॉलर को छोड़ें और अपनी ऐप्स सूची से पंक्ति हटाएँ? डाउनलोड किया गया APK हटा दिया जाएगा। लंबित इंस्टॉल छोड़ें? अनुमति दें @@ -1125,7 +985,6 @@ नया %1$s में नया क्या है नया क्या है - चेंजलॉग अंग्रेज़ी में है। Issues के माध्यम से अनुवाद का स्वागत है। संस्करण %1$s · %2$s @@ -1163,7 +1022,6 @@ Windows macOS Linux - अन्य प्लेटफ़ॉर्म — ट्रांसफ़र के लिए सहेजने हेतु ब्राउज़र में खुलता है आपका डिवाइस ट्रांसफ़र के लिए Android को खुला रखें diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 57bbf7d41..de489724d 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -3,8 +3,6 @@ GitHub Store - - App Installate Indietro Controlla Aggiornamenti @@ -12,7 +10,6 @@ Impossibile lanciare %1$s Impossibile aprire %1$s Impossibile aggiornare %1$s: %2$s - Aggiornamento fallito Aggiornamento generale fallito: %1$s Tutte le app sono state aggiornate con successo Nessun aggiornamento disponibile @@ -61,12 +58,9 @@ Accedi con GitHub - Annullato Errore sconosciuto - Lingua: - Scopri Repositories Cerca repo, descrizione… @@ -115,34 +109,21 @@ Profilo - - ASPETTO - LINGUA - Sovrascrivi la lingua dell'interfaccia. - Lingua dell'App - Cambia menu, pulsanti e messaggi in tutta l'app. Non modifica i contenuti provenienti da GitHub. Segui il sistema Riavvia per applicare la nuova lingua. Riavvia - INFORMAZIONI - RETE Colore del Tema Tema Amoled Nero Nero puro per il tema scuro - Colore selezionato: %1$s - - Versione - Aiuto & Supporto Esci Uscito con successo, reindirizzamento… - Cache cancellata con successo Attenzione! @@ -160,8 +141,6 @@ Annulla download Mostra opzioni di installazione - - Errore nel caricamento dei dettagli Riprova Nessuna descrizione fornita. Nessuna nota di aggiornamento. @@ -170,7 +149,6 @@ Informazioni su quest'app Installa logs - Autore Cosa c'è di nuovo @@ -178,8 +156,6 @@ Aggiornamento disponibile Non disponibile Installa l'ultima versione - Reinstalla - Aggiorna app Scaricamento @@ -208,14 +184,9 @@ Nessun altro risultato su GitHub Ricerca GitHub fallita. Riprova. - - per %1$s - • Installate: %1$s Architettura compatibile Aggiorna a %1$s - - Impossibile caricare i dettagli L'installer è stato salvato nella cartella di download @@ -241,30 +212,13 @@ Errore: %1$s - Tipo di archivio .%1$s non supportato - File scaricato non trovato - In tendenza - Rilascio a caldo - I più popolari - Privacy - Media - Produttività - Rete - Strumenti di sviluppo Ricerca di repositories... - Caricamento in corso... - Non ci sono più repositories Riprova Impossibile caricare repositories - Vedi Dettagli - appena aggiornata - aggiornata %1$d ora/e fa - aggiornata ieri - aggiornata %1$d giorno/i fa aggiornata il %1$s Superato limite di richieste @@ -309,8 +263,6 @@ Chiudi Impossibile sincronizzare i repository preferiti - Profilo dello sviluppatore - Apri il profilo dello sviluppatore Impossibile caricare i repository @@ -324,7 +276,6 @@ Cerca repository… Cancella ricerca - Tutti Con release Installati Preferiti @@ -336,7 +287,6 @@ repository - repository Visualizzazione di %1$d su %2$d repository @@ -344,8 +294,6 @@ Nessun repository installato Nessun repository preferito - - Aggiornato %1$s fa Ha una release @@ -403,35 +351,21 @@ Ultimo controllo: %1$s - Mai controllato proprio ora %1$d min fa %1$d h fa Controllo aggiornamenti… - - Tipo di proxy - Nessuno - Sistema - HTTP - SOCKS Host Porta Nome utente (facoltativo) Password (facoltativo) Salva Proxy Impostazioni proxy salvate - Usa le impostazioni proxy del dispositivo - La porta deve essere 1–65535 - Connessione diretta, nessun proxy Impossibile salvare le impostazioni del proxy L'host del proxy è obbligatorio Inserisci un nome host o indirizzo IP valido Porta proxy non valida - Mostra password - Nascondi password - Verifica - Verifica in corso… Connessione OK (%1$d ms) Impossibile risolvere l'host. Controlla l'indirizzo del proxy. Impossibile raggiungere il server proxy. @@ -439,20 +373,8 @@ Autenticazione proxy richiesta. Risposta inattesa: HTTP %1$d Verifica connessione non riuscita - Ogni categoria può utilizzare il proprio proxy. Configurali in modo indipendente. - Scoperta (API GitHub) - Home, ricerca, dettagli repo e controlli aggiornamenti - Download - Download APK e aggiornamenti automatici - Traduzione - Servizio di traduzione README - - Traccia questa app - App aggiunta alla lista di monitoraggio - Impossibile tracciare l'app: %1$s - L'app è già monitorata Accedi a GitHub @@ -469,7 +391,6 @@ Accedi di nuovo Continua come ospite Questo cancellerà la sessione locale e i dati nella cache. Per revocare completamente l'accesso, visita GitHub Settings > Applications. - Il codice scade tra %1$s Il codice del dispositivo è scaduto. Riprova ad accedere per ottenere un nuovo codice. Controlla la tua connessione internet e riprova. @@ -486,27 +407,16 @@ Impossibile condividere il link Link copiato negli appunti - - Traduci - Traduzione… - Mostra originale - Tradotto in %1$s Traduci in… Cerca lingua - Cambia lingua Traduzione fallita. Riprova. Apri link GitHub Link GitHub rilevato negli appunti - Rileva link dagli appunti - Rileva automaticamente i link GitHub dagli appunti all'apertura della ricerca Link rilevati Apri nell'app Nessun link GitHub trovato negli appunti - Archiviazione - Pulisci cache - Dimensione attuale: Pulisci @@ -522,8 +432,6 @@ - - Installazione Predefinito Finestra di installazione standard del sistema Shizuku @@ -540,7 +448,6 @@ Aggiornamento automatico Scarica e installa automaticamente gli aggiornamenti in background tramite Shizuku - Aggiornamenti Intervallo di controllo Ogni quanto verificare gli aggiornamenti in background Verifica aggiornamenti in background @@ -564,18 +471,13 @@ Validazione… Collega e monitora Controllo dell'ultima versione… - Download APK per la verifica… Verifica della chiave di firma… Nome pacchetto diverso: l'APK è %1$s, ma l'app selezionata è %2$s Chiave di firma diversa: l'APK di questo repository è stato firmato da uno sviluppatore diverso Seleziona installatore Scegli l'APK da verificare con la tua app installata - Download fallito Esporta Importa - Importa app - Incolla il JSON esportato per ripristinare le app monitorate - Incolla il JSON esportato qui… Canale beta predefinito Le app appena tracciate includono build beta per impostazione predefinita. Le app già tracciate mantengono il proprio canale (modificalo nella schermata Dettagli). Disinstallare l'app? @@ -588,9 +490,6 @@ %1$s collegata a %2$s/%3$s Esportazione fallita: %1$s Importazione fallita: %1$s - %1$d app importate - , %1$d saltate - , %1$d fallite Chiave di firma cambiata Il certificato di firma di questa app è cambiato dalla prima installazione.\n\nQuesto potrebbe significare che lo sviluppatore ha cambiato la chiave di firma, o il binario potrebbe essere stato alterato.\n\nPrevisto: %1$s\nRicevuto: %2$s Installa comunque @@ -603,8 +502,6 @@ Risorse multiple disponibili Ci sono più file installabili disponibili per questa versione. Controlla la lista e seleziona quello adatto al tuo dispositivo. Informazioni - Riprova - Rilevato automaticamente: %1$s Seleziona lingua Pacchetto non corrispondente: l'APK è %1$s, ma l'app installata è %2$s. Aggiornamento bloccato. Chiave di firma non corrispondente: l'aggiornamento è stato firmato da uno sviluppatore diverso. Aggiornamento bloccato. @@ -617,21 +514,13 @@ Ampio Extra ampio - Nascondi repository visualizzati - Nascondi i repository già visualizzati dai feed di scoperta - Cancella cronologia visualizzazioni - Reimposta tutti i repository visualizzati in modo che ricompaiano nei feed Cronologia visualizzazioni cancellata Visualizzato Sei il proprietario di questo repo Nascondi repository - Repository nascosto - Annulla Visualizzati di recente Repository che hai visitato - Pacchetti scaricati - APK e installer dalle release GitHub Elimina tutto Eliminare tutti i download? Questo rimuoverà definitivamente tutti gli APK e installer (%1$s). Puoi riscaricarli in qualsiasi momento. @@ -642,9 +531,7 @@ Cancella tutto Rimuovi - Modifiche Modifiche - Pre-release Filtro asset @@ -704,8 +591,6 @@ Pronto per l'installazione Installa (pronto) - - Traduzione Scegli il servizio usato per tradurre i README. Servizio di traduzione Google funziona ovunque senza configurazione. Youdao funziona dalla Cina continentale ma richiede credenziali API dal portale sviluppatori di Youdao. @@ -748,8 +633,6 @@ ghp_… o github_pat_… Accedi Annulla - Mostra token - Nascondi token Incolla un token. Non sembra un token GitHub. I token iniziano con ghp_ o github_pat_. Questo token non è valido o è stato revocato. @@ -771,13 +654,11 @@ Scegli il canale di rilascio Tocca per passare tra rilasci stabili e build beta per questa app. Ho capito - Attiva/disattiva le versioni beta per questa app Passa a %1$s stabile Nessuna versione stabile da %1$d mesi Nessuna versione stabile da %1$d giorni Ci sono pre-release attive, ma il progetto non ha pubblicato una build stabile da un po'. Le versioni beta potrebbero non convergere a un rilascio stabile. Novità da %1$s - — %1$s — Scollega da questo repository - Altre opzioni Scollegare questa app? Smetteremo di tracciare %1$s come installata da questo repository. L'app rimane sul dispositivo — viene rimosso solo il collegamento. Scollega @@ -891,7 +770,6 @@ - Invia feedback Invia feedback Chiudi Categoria @@ -929,15 +807,12 @@ ═══════════════════════════════════════════════════════════════ --> Forge personalizzate - Aggiungi host Forgejo / Gitea che vuoi che GHS riconosca - %1$d aggiunte Forge personalizzate Inserisci l'hostname di un'istanza Forgejo o Gitea (es. git.example.com). GHS accetterà URL di questi host nel modulo di collegamento manuale. Aggiungi Nessuna forge personalizzata. Fatto - Mirror di download Mirror di download Utilizzato per scaricare i file delle release. Le chiamate API di GitHub passano sempre in modo diretto. La maggior parte degli utenti dovrebbe lasciare questa impostazione su GitHub diretto. Ufficiale @@ -953,7 +828,6 @@ Il modello deve contenere {url} esattamente una volta Salva Testa selezionato - Test in corso… Raggiunto in %1$dms Il mirror ha restituito %1$d Timeout dopo 5s @@ -961,12 +835,6 @@ Errore: %1$s Tutti i mirror non disponibili? Puoi ospitarne uno tuo in 5 minuti — consulta la documentazione. %1$s non è più disponibile, passato a GitHub diretto. - Checksum non corrispondente — il file potrebbe essere stato manomesso - Provare un mirror più veloce? - Alcuni utenti con connessioni lente hanno risultati migliori con un proxy della comunità. - Scegli uno - Forse più tardi - Non chiedere di nuovo Aggiungi dai preferiti @@ -977,13 +845,7 @@ Sicurezza Stato Sondaggi - Chiudi dopo presa visione - %1$d g Nulla da condividere al momento. - %1$d h - proprio ora - Aggiornato %1$s fa - %1$d min Non può essere disattivato Mostra elementi da Apri impostazioni silenzia @@ -1040,7 +902,6 @@ Compresso Espanso Aggiornato - Aggiornamenti disponibili Scartare l'installer parcheggiato per %1$s e rimuovere la riga dalla tua lista app? L'APK scaricato verrà eliminato. Scartare l'installazione in attesa? Concedi permesso @@ -1126,7 +987,6 @@ Novità Novità in %1$s Cosa c'è di nuovo - Il changelog è in inglese. Le traduzioni sono benvenute tramite Issues. Versione %1$s · %2$s @@ -1164,7 +1024,6 @@ Windows macOS Linux - Altra piattaforma — si apre nel browser per salvare e trasferire Il tuo dispositivo Da trasferire Mantieni Android aperto diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 116df7ad7..37fbbc39b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1,14 +1,12 @@ GitHub Store - インストール済みアプリ 戻る 更新を確認 %1$s を起動できません %1$s を開けませんでした %1$s の更新に失敗しました:%2$s - 更新に失敗しました すべての更新に失敗しました:%1$s すべてのアプリが更新されました 更新はありません @@ -48,12 +46,9 @@ GitHubでサインイン - キャンセルされました 不明なエラー - 言語: - リポジトリを探索 リポジトリや説明を検索… 言語でフィルター @@ -98,29 +93,18 @@ プロフィール - 外観 - 言語 - アプリのUI言語を上書きします。 - アプリの言語 - アプリ全体のメニュー、ボタン、メッセージを変更します。GitHubからのコンテンツは変更されません。 システムに従う 新しい言語を適用するには再起動してください。 再起動 - 情報 - ネットワーク テーマカラー AMOLED ブラックテーマ ダークモード用の純黒背景 - 選択された色:%1$s - バージョン - ヘルプとサポート ログアウト ログアウトしました。リダイレクト中… - キャッシュを正常にクリアしました 警告! ログアウトしてもよろしいですか? @@ -130,17 +114,14 @@ Cream Plum - 詳細の読み込みに失敗しました このアプリについて インストールログ - 作者 新機能 問題を報告 インストール済み 更新あり 最新版をインストール - 再インストール ダウンロード中 更新中 @@ -159,12 +140,9 @@ GitHubにこれ以上の結果はありません GitHubからの取得に失敗しました。再試行してください。 - %1$s 作 - • インストール済み: %1$s アーキテクチャ互換 %1$s に更新 - 詳細を読み込めませんでした インストーラーはダウンロードに保存されました ダウンロード開始 @@ -188,30 +166,13 @@ サードパーティアプリを使用してAPKをインストール エラー: %1$s - ファイル形式 .%1$s は未対応です - ダウンロードしたファイルが見つかりません - トレンド - ホットリリース - 最も人気のある - プライバシー - メディア - 生産性 - ネットワーク - 開発ツール リポジトリを検索中… - 読み込み中… - これ以上ありません 再試行 リポジトリの読み込みに失敗しました - 詳細を見る - たった今更新 - %1$d時間前に更新 - 昨日更新 - %1$d日前に更新 %1$s に更新 レート制限を超えました @@ -260,8 +221,6 @@ 閉じる スター付きリポジトリの同期に失敗しました - 開発者プロフィール - 開発者プロフィールを開く リポジトリの読み込みに失敗しました @@ -275,7 +234,6 @@ リポジトリを検索… 検索をクリア - すべて リリースあり インストール済み お気に入り @@ -287,7 +245,6 @@ リポジトリ - リポジトリ %2$d件中%1$d件のリポジトリを表示 @@ -295,8 +252,6 @@ インストール済みのリポジトリがありません お気に入りのリポジトリがありません - - %1$sに更新 リリースあり @@ -343,7 +298,6 @@ 利用不可 - アプリを更新 インストール待ち @@ -367,35 +321,21 @@ 最終確認: %1$s - 未確認 たった今 %1$d分前 %1$d時間前 アップデートを確認中… - - プロキシの種類 - なし - システム - HTTP - SOCKS ホスト ポート ユーザー名(任意) パスワード(任意) プロキシを保存 プロキシ設定を保存しました - デバイスのプロキシ設定を使用します - ポートは1〜65535の範囲で指定してください - 直接接続、プロキシなし プロキシ設定の保存に失敗しました プロキシホストは必須です 有効なホスト名または IP アドレスを入力してください 無効なプロキシポート - パスワードを表示 - パスワードを非表示 - テスト - テスト中… 接続OK (%1$d ms) ホストを解決できません。プロキシアドレスを確認してください。 プロキシサーバーに接続できません。 @@ -403,20 +343,8 @@ プロキシ認証が必要です。 予期しない応答:HTTP %1$d 接続テストに失敗しました - 各カテゴリは独自のプロキシを使用できます。個別に設定してください。 - ディスカバリー (GitHub API) - ホーム、検索、リポジトリ詳細、更新確認 - ダウンロード - APK ダウンロードと自動更新 - 翻訳 - README 翻訳サービス - - このアプリを追跡 - アプリを追跡リストに追加しました - アプリの追跡に失敗しました: %1$s - このアプリは既に追跡中です GitHubにサインイン @@ -433,7 +361,6 @@ 再度サインイン ゲストとして続行 ローカルセッションとキャッシュデータが消去されます。アクセスを完全に取り消すには、GitHub Settings > Applicationsにアクセスしてください。 - コードの有効期限: %1$s デバイスコードの有効期限が切れました。 新しいコードを取得するために再度サインインしてください。 インターネット接続を確認して再試行してください。 @@ -450,27 +377,16 @@ リンクの共有に失敗しました リンクをクリップボードにコピーしました - - 翻訳 - 翻訳中… - 原文を表示 - %1$sに翻訳済み 翻訳先… 言語を検索 - 言語を変更 翻訳に失敗しました。もう一度お試しください。 GitHubリンクを開く クリップボードにGitHubリンクを検出 - クリップボードリンクの自動検出 - 検索画面を開く際にクリップボードからGitHubリンクを自動検出 検出されたリンク アプリで開く クリップボードにGitHubリンクが見つかりません - ストレージ - キャッシュをクリア - 現在のサイズ: クリア @@ -485,8 +401,6 @@ - - インストール デフォルト 標準のシステムインストールダイアログ Shizuku @@ -503,7 +417,6 @@ アプリを自動更新 Shizukuを使用してバックグラウンドで自動的にアップデートをダウンロードしてインストール - アップデート アップデート確認間隔 バックグラウンドでアプリのアップデートを確認する頻度 バックグラウンド更新確認 @@ -527,18 +440,13 @@ 検証中… リンクして追跡 最新リリースを確認中… - 検証用APKをダウンロード中… 署名キーを検証中… パッケージ名が一致しません:APKは%1$sですが、選択されたアプリは%2$sです 署名キーが一致しません:このリポジトリのAPKは別の開発者によって署名されています インストーラーを選択 インストール済みアプリと照合するAPKを選択 - ダウンロード失敗 エクスポート インポート - アプリをインポート - エクスポートしたJSONを貼り付けて追跡中のアプリを復元 - エクスポートしたJSONをここに貼り付け… デフォルトのベータチャンネル 新しく追跡したアプリはデフォルトでベータビルドを含みます。既に追跡中のアプリは独自の設定を保持します(アプリの詳細画面で変更可能)。 アプリをアンインストールしますか? @@ -551,9 +459,6 @@ %1$sを%2$s/%3$sにリンクしました エクスポート失敗:%1$s インポート失敗:%1$s - %1$d個のアプリをインポート - 、%1$d個スキップ - 、%1$d個失敗 署名キーが変更されました このアプリの署名証明書が初回インストール以降に変更されました。\n\nこれは開発者が署名キーを変更したか、バイナリが改ざんされた可能性があります。\n\n期待値:%1$s\n受信値:%2$s それでもインストール @@ -566,8 +471,6 @@ 複数のアセットが利用可能 このリリースには複数のインストール可能なファイルがあります。リストを確認し、お使いのデバイスに合ったものを選択してください。 情報 - 再試行 - 自動検出:%1$s 言語を選択 パッケージの不一致: APKは%1$sですが、インストール済みアプリは%2$sです。更新がブロックされました。 署名キーの不一致: 更新は別の開発者によって署名されています。更新がブロックされました。 @@ -580,21 +483,13 @@ ワイド 超ワイド - 閲覧済みリポジトリを非表示 - すでに閲覧したリポジトリをディスカバリーフィードから非表示にします - 閲覧履歴をクリア - すべての閲覧済みリポジトリをリセットしてフィードに再表示します 閲覧履歴をクリアしました 閲覧済み あなたが所有するリポジトリ リポジトリを非表示 - リポジトリを非表示にしました - 元に戻す 最近閲覧した 訪問したリポジトリ - ダウンロード済みパッケージ - GitHubリリースからのAPKとインストーラー すべて削除 すべてのダウンロードを削除しますか? すべてのAPKとインストーラーが完全に削除されます(%1$s)。いつでも再ダウンロードできます。 @@ -605,9 +500,7 @@ すべてクリア 削除 - 調整 調整 - プレリリース アセットフィルター @@ -665,8 +558,6 @@ インストール準備完了 インストール(準備完了) - - 翻訳 README の翻訳に使用するサービスを選択します。 翻訳サービス Google は世界中で設定不要で動作します。Youdao は中国本土からアクセスできますが、Youdao 開発者ポータルから API 認証情報が必要です。 @@ -709,8 +600,6 @@ ghp_… または github_pat_… ログイン キャンセル - トークンを表示 - トークンを非表示 トークンを貼り付けてください。 GitHub のトークンではないようです。トークンは ghp_ または github_pat_ で始まります。 このトークンは無効か、取り消されています。 @@ -731,13 +620,11 @@ リリースチャンネルを選択 このアプリの安定版とベータビルドを切り替えるにはタップ。 了解 - このアプリのベータリリースを切り替え %1$s の安定版に切り替え %1$d か月間、安定版リリースなし %1$d 日間、安定版リリースなし プレリリースは活発ですが、プロジェクトはしばらく安定版をリリースしていません。ベータ版が安定版リリースに至らない可能性があります。 %1$s 以降の変更点 - — %1$s — このリポジトリからリンクを解除 - その他のオプション このアプリのリンクを解除しますか? このリポジトリからインストールされたものとして %1$s の追跡を停止します。アプリはデバイスに残ります — リンクのみ削除されます。 リンクを解除 @@ -846,7 +731,6 @@ - フィードバックを送信 フィードバックを送信 閉じる カテゴリ @@ -884,15 +768,12 @@ ═══════════════════════════════════════════════════════════════ --> カスタムフォージ - GHSに認識させたいForgejo / Giteaホストを追加 - %1$d 件追加済み カスタムフォージ ForgejoまたはGiteaインスタンスのホスト名を入力(例: git.example.com)。手動リンクシートでこれらのホストのURLが受け入れられます。 追加 カスタムフォージはまだありません。 完了 - ダウンロードミラー ダウンロードミラー リリースアセットのダウンロードに使用されます。GitHub API の呼び出しは常に直接行われます。ほとんどのユーザーは「GitHub 直接」のままにしておくことをお勧めします。 公式 @@ -908,7 +789,6 @@ テンプレートには {url} がちょうど 1 回含まれている必要があります 保存 選択したものをテスト - テスト中… %1$dms で到達 ミラーが %1$d を返しました 5s 後にタイムアウト @@ -916,12 +796,6 @@ 失敗: %1$s すべてのミラーが使えない場合は、5 分で自前のミラーをホストできます — ドキュメントをご覧ください。 %1$s は利用できなくなりました。GitHub 直接に切り替えました。 - チェックサムが一致しません — ファイルが改ざんされている可能性があります - より速いミラーを試しますか? - 低速なネットワークを使用している一部のユーザーには、コミュニティプロキシの方が効果的な場合があります。 - 選択する - 後で - 今後表示しない スター済みから追加 @@ -932,13 +806,7 @@ セキュリティ ステータス アンケート - 確認後に閉じる - %1$d日 いま共有することはありません。 - %1$d時間 - たった今 - %1$s前に更新 - %1$d分 無効化できません 表示するアイテム ミュート設定を開く @@ -995,7 +863,6 @@ 折りたたみ 展開 最新 - アップデートあり %1$sの保留中インストーラーを破棄してアプリ一覧から行を削除しますか?ダウンロード済みのAPKは削除されます。 保留中のインストールを破棄しますか? 権限を付与 @@ -1081,7 +948,6 @@ 新機能 %1$sの新機能 新機能 - 変更履歴は英語です。Issuesでの翻訳投稿を歓迎します。 バージョン %1$s · %2$s @@ -1119,7 +985,6 @@ Windows macOS Linux - 他のプラットフォーム — ブラウザで開き、転送用に保存します このデバイス 転送用 Android をオープンに diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index ac7e0fedd..4a2f81736 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -3,8 +3,6 @@ GitHub Store - - 설치된 앱 뒤로 이동 업데이트 확인 @@ -12,7 +10,6 @@ %1$s을(를) 실행할 수 없습니다 %1$s을(를) 열지 못했습니다 %1$s 업데이트 실패: %2$s - 업데이트 실패 모두 업데이트 실패: %1$s 모든 앱이 성공적으로 업데이트되었습니다 사용 가능한 업데이트가 없습니다 @@ -60,12 +57,9 @@ GitHub로 로그인 - 취소됨 알 수 없는 오류 - 언어: - 저장소 탐색 저장소, 설명 검색… @@ -113,34 +107,21 @@ 프로필 - - 외관 - 언어 - 앱의 UI 언어를 재정의합니다. - 앱 언어 - 앱 전체의 메뉴, 버튼, 메시지를 변경합니다. GitHub의 콘텐츠는 변경하지 않습니다. 시스템 따름 새 언어를 적용하려면 다시 시작하세요. 다시 시작 - 정보 - 네트워크 테마 색상 AMOLED 블랙 테마 다크 모드용 순수 블랙 배경 - 선택된 색상: %1$s - - 버전 - 도움말 및 지원 로그아웃 성공적으로 로그아웃되었습니다. 이동 중... - 캐시가 성공적으로 삭제되었습니다 경고! @@ -158,8 +139,6 @@ 다운로드 취소 설치 옵션 보기 - - 상세 정보를 불러오는 중 오류 발생 다시 시도 설명이 없습니다. 릴리스 노트가 없습니다. @@ -168,7 +147,6 @@ 이 앱 정보 설치 로그 - 작성자 새로운 기능 @@ -176,8 +154,6 @@ 업데이트 가능 사용 불가 최신 버전 설치 - 재설치 - 앱 업데이트 다운로드 중 @@ -206,14 +182,9 @@ GitHub에 더 이상 결과가 없습니다 GitHub에서 가져오기 실패. 다시 시도하세요. - - %1$s 작성 - • 설치됨: %1$s 아키텍처 호환 %1$s(으)로 업데이트 - - 상세 정보를 불러오지 못했습니다 설치 파일이 다운로드 폴더에 저장되었습니다 @@ -239,30 +210,13 @@ 오류: %1$s - .%1$s 파일 형식은 지원되지 않습니다 - 다운로드된 파일을 찾을 수 없습니다 - 인기 - 핫 릴리스 - 가장 인기 있는 - 개인정보 - 미디어 - 생산성 - 네트워크 - 개발 도구 저장소를 찾는 중... - 더 불러오는 중... - 더 이상 저장소가 없습니다 다시 시도 저장소를 불러오지 못했습니다 - 상세 보기 - 방금 업데이트됨 - %1$d시간 전 업데이트됨 - 어제 업데이트됨 - %1$d일 전 업데이트됨 %1$s에 업데이트됨 요청 한도 초과 @@ -311,8 +265,6 @@ 닫기 별표 저장소 동기화에 실패했습니다 - 개발자 프로필 - 개발자 프로필 열기 저장소를 불러오지 못했습니다 @@ -326,7 +278,6 @@ 저장소 검색… 검색 지우기 - 전체 릴리스 포함 설치됨 즐겨찾기 @@ -338,7 +289,6 @@ 저장소 - 저장소 총 %2$d개 중 %1$d개의 저장소 표시 @@ -346,8 +296,6 @@ 설치된 저장소가 없습니다 즐겨찾기 저장소가 없습니다 - - %1$s 업데이트됨 릴리스 있음 @@ -400,35 +348,21 @@ 마지막 확인: %1$s - 확인한 적 없음 방금 %1$d분 전 %1$d시간 전 업데이트 확인 중… - - 프록시 유형 - 없음 - 시스템 - HTTP - SOCKS 호스트 포트 사용자 이름 (선택 사항) 비밀번호 (선택 사항) 프록시 저장 프록시 설정이 저장되었습니다 - 기기의 프록시 설정을 사용합니다 - 포트는 1–65535 사이여야 합니다 - 직접 연결, 프록시 없음 프록시 설정을 저장하지 못했습니다 프록시 호스트가 필요합니다 유효한 호스트 이름 또는 IP 주소를 입력하세요 잘못된 프록시 포트 - 비밀번호 표시 - 비밀번호 숨기기 - 테스트 - 테스트 중… 연결 성공 (%1$d ms) 호스트를 확인할 수 없습니다. 프록시 주소를 확인하세요. 프록시 서버에 연결할 수 없습니다. @@ -436,20 +370,8 @@ 프록시 인증이 필요합니다. 예기치 않은 응답: HTTP %1$d 연결 테스트 실패 - 각 카테고리는 자체 프록시를 사용할 수 있습니다. 독립적으로 구성하십시오. - 탐색 (GitHub API) - 홈, 검색, 저장소 상세, 업데이트 확인 - 다운로드 - APK 다운로드 및 자동 업데이트 - 번역 - README 번역 서비스 - - 이 앱 추적 - 앱이 추적 목록에 추가되었습니다 - 앱 추적 실패: %1$s - 이미 추적 중인 앱입니다 GitHub에 로그인 @@ -466,7 +388,6 @@ 다시 로그인 게스트로 계속 로컬 세션과 캐시 데이터가 삭제됩니다. 접근을 완전히 취소하려면 GitHub Settings > Applications를 방문하세요. - 코드 만료까지 %1$s 디바이스 코드가 만료되었습니다. 새 코드를 받으려면 다시 로그인해 주세요. 인터넷 연결을 확인하고 다시 시도하세요. @@ -483,27 +404,16 @@ 링크 공유에 실패했습니다 링크가 클립보드에 복사되었습니다 - - 번역 - 번역 중… - 원문 보기 - %1$s로 번역됨 번역 대상… 언어 검색 - 언어 변경 번역 실패. 다시 시도해주세요. GitHub 링크 열기 클립보드에서 GitHub 링크 감지됨 - 클립보드 링크 자동 감지 - 검색을 열 때 클립보드에서 GitHub 링크를 자동으로 감지 감지된 링크 앱에서 열기 클립보드에서 GitHub 링크를 찾을 수 없습니다 - 저장 공간 - 캐시 지우기 - 현재 크기: 지우기 @@ -520,8 +430,6 @@ - - 설치 기본 표준 시스템 설치 대화 상자 Shizuku @@ -538,7 +446,6 @@ 앱 자동 업데이트 Shizuku를 통해 백그라운드에서 자동으로 업데이트 다운로드 및 설치 - 업데이트 업데이트 확인 주기 백그라운드에서 앱 업데이트를 확인하는 빈도 백그라운드 업데이트 확인 @@ -562,18 +469,13 @@ 확인 중… 연결 및 추적 최신 릴리스 확인 중… - 확인용 APK 다운로드 중… 서명 키 확인 중… 패키지 이름 불일치: APK는 %1$s이지만 선택된 앱은 %2$s입니다 서명 키 불일치: 이 저장소의 APK는 다른 개발자가 서명했습니다 설치 파일 선택 설치된 앱과 대조할 APK를 선택하세요 - 다운로드 실패 내보내기 가져오기 - 앱 가져오기 - 내보낸 JSON을 붙여넣어 추적 중인 앱을 복원하세요 - 내보낸 JSON을 여기에 붙여넣기… 기본 베타 채널 새로 추적한 앱은 기본적으로 베타 빌드를 포함합니다. 이미 추적 중인 앱은 자체 설정을 유지합니다(앱 세부 정보 화면에서 전환). 앱을 제거하시겠습니까? @@ -586,9 +488,6 @@ %1$s이(가) %2$s/%3$s에 연결됨 내보내기 실패: %1$s 가져오기 실패: %1$s - %1$d개의 앱을 가져왔습니다 - , %1$d개 건너뜀 - , %1$d개 실패 서명 키가 변경됨 이 앱의 서명 인증서가 처음 설치된 이후 변경되었습니다.\n\n개발자가 서명 키를 교체했거나 바이너리가 변조되었을 수 있습니다.\n\n예상: %1$s\n수신: %2$s 그래도 설치 @@ -601,8 +500,6 @@ 여러 에셋 사용 가능 이 릴리스에 여러 설치 가능한 파일이 있습니다. 목록을 검토하고 기기에 맞는 파일을 선택하세요. 정보 - 재시도 - 자동 감지: %1$s 언어 선택 패키지 불일치: APK는 %1$s이지만 설치된 앱은 %2$s입니다. 업데이트가 차단되었습니다. 서명 키 불일치: 업데이트가 다른 개발자에 의해 서명되었습니다. 업데이트가 차단되었습니다. @@ -615,21 +512,13 @@ 넓게 매우 넓게 - 본 저장소 숨기기 - 이미 본 저장소를 디스커버리 피드에서 숨깁니다 - 조회 기록 삭제 - 모든 조회 기록을 초기화하여 피드에 다시 표시합니다 조회 기록이 삭제되었습니다 확인함 내가 소유한 저장소 저장소 숨기기 - 저장소가 숨겨졌습니다 - 실행 취소 최근 본 항목 방문한 저장소 - 다운로드된 패키지 - GitHub 릴리스의 APK 및 설치 파일 모두 삭제 모든 다운로드를 삭제하시겠습니까? 모든 APK 및 설치 파일이 영구적으로 삭제됩니다 (%1$s). 언제든 다시 다운로드할 수 있습니다. @@ -640,9 +529,7 @@ 모두 지우기 삭제 - 조정 조정 - 프리릴리스 에셋 필터 @@ -700,8 +587,6 @@ 설치 준비 완료 설치 (준비됨) - - 번역 README 번역에 사용할 서비스를 선택하세요. 번역 서비스 Google은 전 세계적으로 설정 없이 작동합니다. Youdao는 중국 본토에서 작동하지만 Youdao 개발자 포털의 API 자격 증명이 필요합니다. @@ -744,8 +629,6 @@ ghp_… 또는 github_pat_… 로그인 취소 - 토큰 표시 - 토큰 숨기기 토큰을 붙여넣어 주세요. GitHub 토큰이 아닌 것 같습니다. 토큰은 ghp_ 또는 github_pat_로 시작합니다. 이 토큰은 유효하지 않거나 취소되었습니다. @@ -766,13 +649,11 @@ 릴리스 채널 선택 이 앱의 안정 릴리스와 베타 빌드를 전환하려면 탭하세요. 확인 - 이 앱의 베타 릴리스 전환 %1$s 안정 버전으로 전환 %1$d개월간 안정 릴리스 없음 %1$d일간 안정 릴리스 없음 사전 릴리스가 활발하지만 프로젝트가 한동안 안정 빌드를 출시하지 않았습니다. 베타가 안정 릴리스로 이어지지 않을 수 있습니다. %1$s 이후 변경 사항 - — %1$s — 이 저장소에서 연결 해제 - 더 많은 옵션 이 앱의 연결을 해제하시겠습니까? 이 저장소에서 설치된 앱으로 %1$s 추적을 중단합니다. 앱은 기기에 그대로 남습니다 — 연결만 제거됩니다. 연결 해제 @@ -881,7 +760,6 @@ - 피드백 보내기 피드백 보내기 닫기 카테고리 @@ -919,15 +797,12 @@ ═══════════════════════════════════════════════════════════════ --> 사용자 포지 - GHS가 인식할 Forgejo / Gitea 호스트를 추가하세요 - %1$d개 추가됨 사용자 포지 Forgejo 또는 Gitea 인스턴스의 호스트명을 입력하세요 (예: git.example.com). 수동 연결 시트에서 이러한 호스트의 URL을 GHS가 받습니다. 추가 아직 사용자 포지가 없습니다. 완료 - 다운로드 미러 다운로드 미러 릴리스 파일 다운로드에 사용됩니다. GitHub API 호출은 항상 직접 이루어집니다. 대부분의 사용자는 GitHub 직접으로 두는 것이 좋습니다. 공식 @@ -943,7 +818,6 @@ 템플릿에는 {url}이 정확히 한 번 포함되어야 합니다 저장 선택한 항목 테스트 - 테스트 중… %1$dms에 도달 미러가 %1$d를 반환했습니다 5s 후 타임아웃 @@ -951,12 +825,6 @@ 실패: %1$s 모든 미러가 다운되었나요? 5분 안에 직접 호스팅할 수 있습니다 — 문서를 참조하세요. %1$s를 더 이상 사용할 수 없어 GitHub 직접으로 전환했습니다. - 체크섬 불일치 — 파일이 변조되었을 수 있습니다 - 더 빠른 미러를 사용해 볼까요? - 느린 네트워크에서 일부 사용자는 커뮤니티 프록시를 사용할 때 더 나은 경험을 합니다. - 선택하기 - 나중에 - 다시 묻지 않기 별표한 항목에서 추가 @@ -967,13 +835,7 @@ 보안 상태 설문조사 - 확인 후 닫기 - %1$d일 지금 공유할 내용이 없습니다. - %1$d시간 - 방금 전 - %1$s 전에 새로 고침됨 - %1$d분 비활성화할 수 없음 표시할 항목 알림 끄기 설정 열기 @@ -1030,7 +892,6 @@ 접힘 펼침 최신 - 업데이트 사용 가능 %1$s의 보관된 설치 프로그램을 삭제하고 앱 목록에서 행을 제거하시겠습니까? 다운로드된 APK가 삭제됩니다. 대기 중인 설치를 삭제할까요? 권한 부여 @@ -1116,7 +977,6 @@ 신규 %1$s의 새로운 기능 새로운 기능 - 변경 로그는 영어로 제공됩니다. Issues를 통한 번역을 환영합니다. 버전 %1$s · %2$s @@ -1154,7 +1014,6 @@ Windows macOS Linux - 다른 플랫폼 — 전송용 저장을 위해 브라우저에서 열립니다 내 기기 전송용 Android를 열린 상태로 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index cc6146f9a..cc3de13a1 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -2,14 +2,12 @@ GitHub Store - Zainstalowane aplikacje Cofnij Sprawdź aktualizacje Nie można uruchomić %1$s Nie udało się otworzyć %1$s Nie udało się zaktualizować %1$s: %2$s - Aktualizacja nie powiodła się Aktualizacja wszystkich nie powiodła się: %1$s Wszystkie aplikacje zostały pomyślnie zaktualizowane Brak dostępnych aktualizacji @@ -50,12 +48,9 @@ Zaloguj się przez GitHub - Anulowano Nieznany błąd - Język: - Odkrywaj repozytoria Szukaj repozytorium, opisu… Filtruj według języka @@ -99,29 +94,18 @@ Profil - WYGLĄD - JĘZYK - Zmień język interfejsu aplikacji. - Język aplikacji - Zmienia menu, przyciski i komunikaty w całej aplikacji. Nie zmienia treści pochodzącej z GitHub. Taki jak system Uruchom ponownie, aby zastosować nowy język. Uruchom ponownie - O APLIKACJI - SIEĆ Kolor motywu Motyw AMOLED Black Czyste czarne tło dla trybu ciemnego - Wybrany kolor: %1$s - Wersja - Pomoc i wsparcie Wyloguj się Wylogowano pomyślnie, przekierowywanie... - Pamięć podręczna wyczyszczona pomyślnie Ostrzeżenie! Czy na pewno chcesz się wylogować? @@ -137,22 +121,18 @@ Pokaż opcje instalacji Zgłoś problem - Błąd podczas ładowania szczegółów Ponów Brak opisu. Brak informacji o wydaniu. O tej aplikacji Logi instalacji - Autor Co nowego Zainstalowano Dostępna aktualizacja Niedostępne Zainstaluj najnowszą - Zainstaluj ponownie - Aktualizuj aplikację Pobieranie Aktualizowanie @@ -177,12 +157,9 @@ Brak więcej wyników na GitHub Nie udało się pobrać z GitHub. Spróbuj ponownie. - autor: %1$s - • Zainstalowana: %1$s Architektura zgodna Aktualizuj do %1$s - Nie udało się załadować szczegółów Instalator został zapisany w folderze Pobrane Rozpoczęto pobieranie @@ -205,30 +182,13 @@ Użyj aplikacji innej firmy do zainstalowania APK Błąd: %1$s - Typ pliku .%1$s nie jest obsługiwany - Nie znaleziono pobranego pliku - Na czasie - Gorące wydanie - Najpopularniejsze - Prywatność - Media - Produktywność - Sieć - Narzędzia deweloperskie Wyszukiwanie repozytoriów... - Ładowanie więcej... - Brak kolejnych repozytoriów Ponów Nie udało się załadować repozytoriów - Zobacz szczegóły - zaktualizowano przed chwilą - zaktualizowano %1$d godz. temu - zaktualizowano wczoraj - zaktualizowano %1$d dni temu zaktualizowano %1$s Limit zapytań przekroczony @@ -277,8 +237,6 @@ Zamknij Nie udało się zsynchronizować oznaczonych gwiazdką repozytoriów - Profil dewelopera - Otwórz profil dewelopera Nie udało się załadować repozytoriów @@ -292,7 +250,6 @@ Szukaj repozytoriów… Wyczyść wyszukiwanie - Wszystkie Z wydaniami Zainstalowane Ulubione @@ -304,7 +261,6 @@ repozytorium - repozytoriów Wyświetlanie %1$d z %2$d repozytoriów @@ -312,8 +268,6 @@ Brak zainstalowanych repozytoriów Brak ulubionych repozytoriów - - Zaktualizowano %1$s Ma wydanie @@ -365,35 +319,21 @@ Ostatnio sprawdzono: %1$s - Nigdy nie sprawdzano właśnie teraz %1$d min temu %1$d godz. temu Sprawdzanie aktualizacji… - - Typ proxy - Brak - Systemowy - HTTP - SOCKS Host Port Nazwa użytkownika (opcjonalnie) Hasło (opcjonalnie) Zapisz Proxy Ustawienia proxy zostały zapisane - Używa ustawień proxy urządzenia - Port musi być z zakresu 1–65535 - Połączenie bezpośrednie, bez proxy Nie udało się zapisać ustawień proxy Host proxy jest wymagany Wprowadź prawidłową nazwę hosta lub adres IP Nieprawidłowy port proxy - Pokaż hasło - Ukryj hasło - Testuj - Testowanie… Połączenie OK (%1$d ms) Nie można rozwiązać hosta. Sprawdź adres proxy. Nie można połączyć się z serwerem proxy. @@ -401,20 +341,8 @@ Wymagane uwierzytelnienie proxy. Nieoczekiwana odpowiedź: HTTP %1$d Test połączenia nie powiódł się - Każda kategoria może używać własnego proxy. Skonfiguruj je niezależnie. - Odkrywanie (GitHub API) - Strona główna, wyszukiwanie, szczegóły repo i sprawdzanie aktualizacji - Pobieranie - Pobieranie APK i automatyczne aktualizacje - Tłumaczenie - Usługa tłumaczenia README - - Śledź tę aplikację - Aplikacja dodana do listy śledzonych - Nie udało się śledzić aplikacji: %1$s - Aplikacja jest już śledzona Zaloguj się przez GitHub @@ -431,7 +359,6 @@ Zaloguj się ponownie Kontynuuj jako gość Spowoduje to wyczyszczenie lokalnej sesji i danych z pamięci podręcznej. Aby całkowicie cofnąć dostęp, odwiedź GitHub Settings > Applications. - Kod wygasa za %1$s Kod urządzenia wygasł. Spróbuj zalogować się ponownie, aby uzyskać nowy kod. Sprawdź połączenie internetowe i spróbuj ponownie. @@ -448,27 +375,16 @@ Nie udało się udostępnić linku Link skopiowany do schowka - - Tłumacz - Tłumaczenie… - Pokaż oryginał - Przetłumaczono na %1$s Tłumacz na… Szukaj języka - Zmień język Tłumaczenie nie powiodło się. Spróbuj ponownie. Otwórz link GitHub Wykryto link GitHub w schowku - Automatyczne wykrywanie linków ze schowka - Automatycznie wykrywaj linki GitHub ze schowka przy otwieraniu wyszukiwania Wykryte linki Otwórz w aplikacji Nie znaleziono linku GitHub w schowku - Przechowywanie - Wyczyść pamięć podręczną - Aktualny rozmiar: Wyczyść @@ -486,8 +402,6 @@ - - Instalacja Domyślny Standardowe okno instalacji systemowej Shizuku @@ -504,7 +418,6 @@ Automatyczna aktualizacja Automatycznie pobieraj i instaluj aktualizacje w tle przez Shizuku - Aktualizacje Częstotliwość sprawdzania Jak często sprawdzać aktualizacje aplikacji w tle Sprawdzanie aktualizacji w tle @@ -528,18 +441,13 @@ Weryfikacja… Połącz i śledź Sprawdzanie najnowszego wydania… - Pobieranie APK do weryfikacji… Weryfikacja klucza podpisu… Niezgodność nazwy pakietu: APK to %1$s, ale wybrana aplikacja to %2$s Niezgodność klucza podpisu: APK z tego repozytorium został podpisany przez innego programistę Wybierz instalator Wybierz APK do weryfikacji z zainstalowaną aplikacją - Pobieranie nie powiodło się Eksportuj Importuj - Importuj aplikacje - Wklej wyeksportowany JSON, aby przywrócić śledzone aplikacje - Wklej wyeksportowany JSON tutaj… Domyślny kanał beta Nowo śledzone aplikacje domyślnie zawierają wersje beta. Już śledzone aplikacje zachowują własne ustawienie (zmień je na ekranie Szczegółów aplikacji). Odinstalować aplikację? @@ -552,9 +460,6 @@ %1$s połączono z %2$s/%3$s Eksport nie powiódł się: %1$s Import nie powiódł się: %1$s - Zaimportowano %1$d aplikacji - , %1$d pominięto - , %1$d nie powiodło się Klucz podpisu zmieniony Certyfikat podpisu tej aplikacji zmienił się od pierwszej instalacji.\n\nMoże to oznaczać, że programista zmienił klucz podpisu lub plik binarny mógł zostać zmodyfikowany.\n\nOczekiwano: %1$s\nOtrzymano: %2$s Zainstaluj mimo to @@ -567,8 +472,6 @@ Dostępnych wiele zasobów Dla tego wydania dostępnych jest wiele plików do zainstalowania. Przejrzyj listę i wybierz odpowiedni dla swojego urządzenia. Informacje - Ponów - Wykryto automatycznie: %1$s Wybierz język Niezgodność pakietu: APK to %1$s, ale zainstalowana aplikacja to %2$s. Aktualizacja zablokowana. Niezgodność klucza podpisu: aktualizacja została podpisana przez innego programistę. Aktualizacja zablokowana. @@ -581,21 +484,13 @@ Szeroka Bardzo szeroka - Ukryj przeglądane repozytoria - Ukryj repozytoria, które już przeglądałeś, z kanałów odkrywania - Wyczyść historię przeglądania - Zresetuj wszystkie przeglądane repozytoria, aby ponownie pojawiły się w kanałach Historia przeglądania wyczyszczona Przeglądane Jesteś właścicielem tego repo Ukryj repozytorium - Repozytorium ukryte - Cofnij Ostatnio oglądane Repozytoria, które odwiedziłeś - Pobrane pakiety - APK i instalatory z wydań GitHub Usuń wszystko Usunąć wszystkie pobrania? Spowoduje to trwałe usunięcie wszystkich APK i instalatorów (%1$s). Możesz je pobrać ponownie w dowolnym momencie. @@ -606,9 +501,7 @@ Wyczyść wszystko Usuń - Ustawienia Ustawienia - Wersje wstępne Filtr zasobów @@ -672,8 +565,6 @@ Gotowe do instalacji Zainstaluj (gotowe) - - Tłumaczenie Wybierz usługę używaną do tłumaczenia plików README. Usługa tłumaczenia Google działa globalnie bez konfiguracji. Youdao działa z Chin kontynentalnych, ale wymaga poświadczeń API z portalu deweloperskiego Youdao. @@ -716,8 +607,6 @@ ghp_… lub github_pat_… Zaloguj się Anuluj - Pokaż token - Ukryj token Wklej token. To nie wygląda na token GitHuba. Tokeny zaczynają się od ghp_ lub github_pat_. Ten token jest nieprawidłowy lub został unieważniony. @@ -739,13 +628,11 @@ Wybierz kanał wydań Dotknij, aby przełączać między stabilnymi wydaniami a wersjami beta tej aplikacji. OK - Przełącz wydania beta dla tej aplikacji Przejdź do stabilnej wersji %1$s Brak stabilnego wydania od %1$d miesięcy Brak stabilnego wydania od %1$d dni Są aktywne wersje przedpremierowe, ale projekt od dłuższego czasu nie wydał stabilnej kompilacji. Wersje beta mogą nie prowadzić do stabilnego wydania. Co zmieniło się od %1$s - — %1$s — Odłącz od tego repozytorium - Więcej opcji Odłączyć tę aplikację? Przestaniemy śledzić %1$s jako zainstalowaną z tego repozytorium. Aplikacja pozostaje na urządzeniu — usuwane jest tylko połączenie. Odłącz @@ -872,7 +757,6 @@ - Wyślij opinię Wyślij opinię Zamknij Kategoria @@ -910,15 +794,12 @@ ═══════════════════════════════════════════════════════════════ --> Niestandardowe forge - Dodaj hosty Forgejo / Gitea, które GHS ma rozpoznawać - Dodano %1$d Niestandardowe forge Wpisz nazwę hosta instancji Forgejo lub Gitea (np. git.example.com). GHS przyjmie URL z tych hostów w formularzu ręcznego powiązania. Dodaj Brak niestandardowych forge. Gotowe - Serwer lustrzany pobierania Serwer lustrzany pobierania Używany do pobierania plików wydań. Wywołania API GitHub zawsze przechodzą bezpośrednio. Większość użytkowników powinna pozostawić to ustawienie na GitHub bezpośrednio. Oficjalny @@ -934,7 +815,6 @@ Szablon musi zawierać {url} dokładnie jeden raz Zapisz Testuj wybrany - Testowanie… Osiągnięto w %1$dms Serwer lustrzany zwrócił %1$d Przekroczono limit czasu po 5s @@ -942,12 +822,6 @@ Niepowodzenie: %1$s Wszystkie serwery lustrzane niedostępne? Możesz uruchomić własny w 5 minut — sprawdź dokumentację. %1$s nie jest już dostępny, przełączono na GitHub bezpośrednio. - Niezgodność sumy kontrolnej — plik mógł zostać zmodyfikowany - Wypróbować szybszy serwer lustrzany? - Niektórzy użytkownicy z wolnymi sieciami osiągają lepsze wyniki z proxy społeczności. - Wybierz jeden - Może później - Nie pytaj ponownie Dodaj z oznaczonych gwiazdką @@ -958,13 +832,7 @@ Bezpieczeństwo Status Ankiety - Zamknij po potwierdzeniu - %1$d d Aktualnie nic do udostępnienia. - %1$d h - przed chwilą - Odświeżono %1$s temu - %1$d min Nie można wyłączyć Pokaż elementy z Otwórz ustawienia wyciszenia @@ -1021,7 +889,6 @@ Zwinięte Rozwinięte Aktualne - Dostępne aktualizacje Odrzucić zaparkowany instalator dla %1$s i usunąć wiersz z listy aplikacji? Pobrany APK zostanie usunięty. Odrzucić oczekującą instalację? Udziel uprawnienia @@ -1107,7 +974,6 @@ Nowe Co nowego w %1$s Co nowego - Changelog jest w języku angielskim. Tłumaczenia mile widziane przez Issues. Wersja %1$s · %2$s @@ -1145,7 +1011,6 @@ Windows macOS Linux - Inna platforma — otwiera się w przeglądarce, aby zapisać do transferu Twoje urządzenie Do transferu Niech Android pozostanie otwarty diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index e1d7f103b..b584e442f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1,14 +1,12 @@ GitHub Store - Установленные приложения Назад Проверить обновления Не удалось запустить %1$s Не удалось открыть %1$s Не удалось обновить %1$s: %2$s - Ошибка обновления Ошибка обновления всех: %1$s Все приложения успешно обновлены Обновлений нет @@ -50,12 +48,9 @@ Войти через GitHub - Отменено Неизвестная ошибка - Язык: - Поиск репозиториев Поиск по репозиторию, описанию… Фильтр по языку @@ -98,29 +93,18 @@ Профиль - ВНЕШНИЙ ВИД - ЯЗЫК - Переопределить язык интерфейса приложения. - Язык приложения - Изменяет меню, кнопки и сообщения во всём приложении. Не изменяет содержимое с GitHub. Как в системе Перезапустите, чтобы применить новый язык. Перезапустить - О ПРИЛОЖЕНИИ - СЕТЬ Цвет темы AMOLED чёрная тема Чёрный фон для тёмного режима - Выбранный цвет: %1$s - Версия - Помощь и поддержка Выйти Вы успешно вышли, перенаправление... - Кэш успешно очищен Внимание! Вы уверены, что хотите выйти? @@ -133,21 +117,17 @@ Открыть репозиторий Отменить загрузку - Ошибка загрузки данных Повторить Описание отсутствует. Об этом приложении Журнал установки - Автор Что нового Установлено Доступно обновление Недоступно Установить последнюю - Переустановить - Обновить приложение Сообщить о проблеме Загрузка @@ -173,8 +153,6 @@ Больше результатов на GitHub нет Не удалось загрузить с GitHub. Попробуйте снова. - от %1$s - • Установлено: %1$s Совместимо с архитектурой Обновить до %1$s @@ -182,7 +160,6 @@ Открыть в браузере Показать параметры установки - Не удалось загрузить данные Установщик сохранён в папку Загрузки Загрузка начата @@ -206,30 +183,13 @@ Использовать стороннее приложение для установки APK Ошибка: %1$s - Тип файла .%1$s не поддерживается - Загруженный файл не найден - В тренде - Горячий релиз - Самые популярные - Конфиденциальность - Медиа - Продуктивность - Сеть - Инструменты разработчика Поиск репозиториев... - Загрузка... - Больше репозиториев нет Повторить Не удалось загрузить репозитории - Подробнее - обновлено только что - обновлено %1$d ч. назад - обновлено вчера - обновлено %1$d дн. назад обновлено %1$s Превышен лимит запросов @@ -278,8 +238,6 @@ Закрыть Не удалось синхронизировать избранные репозитории - Профиль разработчика - Открытый профиль разработчика Не удалось загрузить репозитории @@ -293,7 +251,6 @@ Поиск репозиториев… Очистить поиск - Все С релизами Установленные Избранные @@ -305,7 +262,6 @@ репозиторий - репозиториев Показано %1$d из %2$d репозиториев @@ -313,8 +269,6 @@ Нет установленных репозиториев Нет избранных репозиториев - - Обновлено %1$s Есть релиз @@ -367,35 +321,21 @@ Последняя проверка: %1$s - Не проверялось только что %1$d мин назад %1$d ч назад Проверка обновлений… - - Тип прокси - Нет - Системный - HTTP - SOCKS Хост Порт Имя пользователя (необязательно) Пароль (необязательно) Сохранить прокси Настройки прокси сохранены - Использует прокси-настройки устройства - Порт должен быть 1–65535 - Прямое подключение, без прокси Не удалось сохранить настройки прокси Требуется хост прокси Введите корректное имя хоста или IP-адрес Недопустимый порт прокси - Показать пароль - Скрыть пароль - Проверить - Проверка… Соединение в порядке (%1$d мс) Не удалось разрешить хост. Проверьте адрес прокси. Не удалось подключиться к прокси-серверу. @@ -403,20 +343,8 @@ Требуется аутентификация прокси. Неожиданный ответ: HTTP %1$d Не удалось проверить соединение - Каждая категория может использовать свой собственный прокси. Настройте их независимо. - Обнаружение (GitHub API) - Главная, поиск, детали репозитория и проверка обновлений - Загрузки - Загрузка APK и автоматические обновления - Перевод - Сервис перевода README - - Отслеживать приложение - Приложение добавлено в список отслеживания - Не удалось отследить приложение: %1$s - Приложение уже отслеживается Войти через GitHub @@ -433,7 +361,6 @@ Войти снова Продолжить как гость Это очистит вашу локальную сессию и кэшированные данные. Чтобы полностью отозвать доступ, перейдите в GitHub Settings > Applications. - Код истекает через %1$s Срок действия кода устройства истёк. Пожалуйста, попробуйте войти снова для получения нового кода. Проверьте подключение к интернету и попробуйте снова. @@ -450,27 +377,16 @@ Не удалось поделиться ссылкой Ссылка скопирована в буфер обмена - - Перевести - Перевод… - Показать оригинал - Переведено на %1$s Перевести на… Поиск языка - Изменить язык Ошибка перевода. Попробуйте ещё раз. Открыть ссылку GitHub Обнаружена ссылка GitHub в буфере обмена - Автоопределение ссылок из буфера - Автоматически определять ссылки GitHub из буфера обмена при открытии поиска Обнаруженные ссылки Открыть в приложении Ссылка GitHub не найдена в буфере обмена - Хранение - Очистить кэш - Текущий размер: Очистить @@ -486,8 +402,6 @@ - - Установка По умолчанию Стандартный системный диалог установки Shizuku @@ -504,7 +418,6 @@ Автообновление приложений Автоматически загружать и устанавливать обновления в фоне через Shizuku - Обновления Интервал проверки обновлений Как часто проверять обновления приложения в фоне Фоновая проверка обновлений @@ -528,18 +441,13 @@ Проверка… Привязать и отслеживать Проверка последнего релиза… - Загрузка APK для проверки… Проверка ключа подписи… Несоответствие имени пакета: APK — %1$s, а выбранное приложение — %2$s Несоответствие ключа подписи: APK в этом репозитории подписан другим разработчиком Выберите установщик Выберите APK для проверки соответствия установленному приложению - Ошибка загрузки Экспорт Импорт - Импорт приложений - Вставьте экспортированный JSON для восстановления отслеживаемых приложений - Вставьте экспортированный JSON… Канал бета по умолчанию Вновь отслеживаемые приложения по умолчанию включают бета-сборки. Ранее отслеживаемые приложения сохраняют своё значение (переключите на экране Подробностей приложения). Удалить приложение? @@ -552,9 +460,6 @@ %1$s привязано к %2$s/%3$s Ошибка экспорта: %1$s Ошибка импорта: %1$s - Импортировано %1$d приложений - , %1$d пропущено - , %1$d с ошибкой Ключ подписи изменён Сертификат подписи этого приложения изменился с момента первой установки.\n\nЭто может означать, что разработчик сменил ключ подписи, или бинарный файл был изменён.\n\nОжидалось: %1$s\nПолучено: %2$s Всё равно установить @@ -567,8 +472,6 @@ Доступно несколько ресурсов Для этого релиза доступно несколько устанавливаемых файлов. Просмотрите список и выберите подходящий для вашего устройства. Информация - Повторить - Автоопределение: %1$s Выбрать язык Несоответствие пакета: APK — %1$s, но установленное приложение — %2$s. Обновление заблокировано. Несоответствие ключа подписи: обновление подписано другим разработчиком. Обновление заблокировано. @@ -581,21 +484,13 @@ Широкая Очень широкая - Скрыть просмотренные репозитории - Скрыть уже просмотренные репозитории из лент обнаружения - Очистить историю просмотров - Сбросить все просмотренные репозитории, чтобы они снова появились в лентах История просмотров очищена Просмотрено Ваш репозиторий Скрыть репозиторий - Репозиторий скрыт - Отменить Недавно просмотренные Репозитории, которые вы посещали - Загруженные пакеты - APK и установщики из релизов GitHub Удалить всё Удалить все загрузки? Это навсегда удалит все APK и установщики (%1$s). Вы сможете скачать их снова в любое время. @@ -606,9 +501,7 @@ Очистить всё Удалить - Настройки Настройки - Пре-релизы Фильтр ассетов @@ -672,8 +565,6 @@ Готово к установке Установить (готово) - - Перевод Выберите сервис для перевода README. Сервис перевода Google работает глобально без настройки. Youdao работает из материкового Китая, но требует учётных данных API с портала разработчиков Youdao. @@ -716,8 +607,6 @@ ghp_… или github_pat_… Войти Отмена - Показать токен - Скрыть токен Вставьте токен. Это не похоже на токен GitHub. Токены начинаются с ghp_ или github_pat_. Этот токен недействителен или отозван. @@ -739,13 +628,11 @@ Выберите канал релизов Нажмите, чтобы переключаться между стабильными релизами и бета-сборками этого приложения. Понятно - Переключить бета-релизы для этого приложения Перейти на стабильную %1$s Нет стабильного релиза уже %1$d месяцев Нет стабильного релиза уже %1$d дней Есть активные предварительные версии, но проект давно не выпускал стабильную сборку. Бета-версии могут так и не перейти в стабильный релиз. Что изменилось с %1$s - — %1$s — Отвязать от этого репозитория - Другие параметры Отвязать это приложение? Мы перестанем отслеживать %1$s как установленное из этого репозитория. Приложение останется на устройстве — удаляется только связь. Отвязать @@ -872,7 +757,6 @@ - Отправить отзыв Отправить отзыв Закрыть Категория @@ -910,15 +794,12 @@ ═══════════════════════════════════════════════════════════════ --> Свои форджи - Добавьте хосты Forgejo / Gitea, которые GHS должен распознавать - Добавлено: %1$d Свои форджи Введите имя хоста Forgejo или Gitea (например git.example.com). GHS будет принимать URL с этих хостов в форме ручной привязки. Добавить Пока нет своих форджей. Готово - Зеркало для загрузки Зеркало для загрузки Используется для загрузки файлов релизов. Обращения к API GitHub всегда идут напрямую. Большинству пользователей следует оставить настройку «Напрямую через GitHub». Официальное @@ -934,7 +815,6 @@ Шаблон должен содержать {url} ровно один раз Сохранить Проверить выбранное - Проверка… Достигнуто за %1$dms Зеркало вернуло %1$d Превышено время ожидания (5s) @@ -942,12 +822,6 @@ Ошибка: %1$s Все зеркала недоступны? За 5 минут можно развернуть своё — смотрите документацию. %1$s больше недоступно, выполнен переход на прямой доступ через GitHub. - Несоответствие контрольной суммы — файл мог быть изменён - Попробовать более быстрое зеркало? - Некоторым пользователям с медленным соединением лучше подходит прокси сообщества. - Выбрать - Может быть, позже - Больше не спрашивать Добавить из избранных @@ -958,13 +832,7 @@ Безопасность Статус Опросы - Закрыть после подтверждения - %1$d дн Сейчас нечем поделиться. - %1$d ч - только что - Обновлено %1$s назад - %1$d мин Нельзя отключить Показывать элементы из Открыть настройки отключения @@ -1021,7 +889,6 @@ Свёрнут Развёрнут Актуально - Доступны обновления Отбросить отложенный установщик для %1$s и удалить строку из списка приложений? Загруженный APK будет удалён. Отбросить ожидающую установку? Предоставить разрешение @@ -1107,7 +974,6 @@ Новое Что нового в %1$s Что нового - Журнал изменений на английском. Переводы приветствуются через Issues. Версия %1$s · %2$s @@ -1145,7 +1011,6 @@ Windows macOS Linux - Другая платформа — открывается в браузере для сохранения и переноса Это устройство Для переноса Сохраним Android открытым diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 769d2de35..16da4369d 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -3,8 +3,6 @@ GitHub Store - - Yüklü Uygulamalar Geri git Güncellemeleri kontrol et @@ -12,7 +10,6 @@ %1$s çalıştırılamadı %1$s açılamadı %1$s: %2$s güncellenemedi - Güncelleme başarısız Tümünü güncelleme başarısız: %1$s Tüm uygulamalar başarılı şekilde güncellendi Güncelleme yok @@ -60,12 +57,9 @@ Github ile giriş yap - İptal edildi Bilinmeyen hata - Dil: - Repoları keşfet Repo, açıklama ara... @@ -114,34 +108,21 @@ Profil - - GÖRÜNÜM - DİL - Uygulama arayüz dilini geçersiz kılar. - Uygulama dili - Uygulamadaki menüleri, düğmeleri ve mesajları değiştirir. GitHub'dan gelen içeriği değiştirmez. Sistemi izle Yeni dili uygulamak için yeniden başlatın. Yeniden başlat - HAKKINDA - Tema Rengi AMOLED Siyah Tema Karanlık mod için saf siyah arka plan - Seçilmiş renk: %1$s - - Sürüm - Yardım & Destek Çıkış Yap Başarılı şekilde çıkış yapıldı, yönlendiriliyor... - Önbellek başarıyla temizlendi Uyarı! @@ -159,8 +140,6 @@ İndirmeyi iptal et Yükleme seçeneklerini göster - - Detaylar yüklenirken hata Tekrar dene Açıklama sağlanmadı. Sürüm notu yok @@ -169,7 +148,6 @@ Bu uygulama hakkında Yükleme günlükleri - Yazar Neler Yeni @@ -177,8 +155,6 @@ Güncelleme mevcut Mevcut değil En son sürümü yükle - Tekrar yükle - Uygulamayı güncelle İndiriliyor @@ -207,14 +183,9 @@ GitHub'ta başka sonuç yok GitHub'tan getirilemedi. Tekrar deneyin. - - %1$s - Yüklenmiş: %1$s Mimari uyumlu %1$s'e güncelle - - Detaylar yüklenemedi Yükleyici, İndirilenler klasörüne kaydedildi @@ -240,30 +211,13 @@ Hata: %1$s - Tür .%1$s desteklenmiyor - İndirilen dosya bulunamadı - Trendler - Yeni sürüm - En popüler - Gizlilik - Medya - Üretkenlik - - Geliştirici araçları Repolar bulunuyor... - Daha fazla yükleniyor... - Başka repo yok Tekrar dene Repolar yüklenemedi - Detayları Görüntüle - şimdi güncellendi - %1$d saat önce güncellendi - dün güncellendi - %1$d gün önce güncellendi %1$s tarihinde güncellendi Hız Sınırı Aşıldı @@ -312,8 +266,6 @@ Kapat Yıldızlı repoları eşitlerken hata - Geliştirici Profili - Geliştirici profilini aç Repolar yüklenirken hata Profil yüklenirken hata @@ -326,7 +278,6 @@ Repo ara… Aramayı temizle - Hepsi Yayınlanmış İndirilmiş Favoriler @@ -338,7 +289,6 @@ repo - repolar %2$d repodan %1$d tanesi gösteriliyor @@ -346,8 +296,6 @@ Yüklü repo yok Favori repo yok - - %1$s güncellendi Yayınlanmış %1$d y önce @@ -399,35 +347,21 @@ Son kontrol: %1$s - Hiç kontrol edilmedi az önce %1$d dk önce %1$d sa önce Güncellemeler kontrol ediliyor… - - Proxy Türü - Yok - Sistem - HTTP - SOCKS Ana Bilgisayar Port Kullanıcı adı (isteğe bağlı) Şifre (isteğe bağlı) Proxy'yi Kaydet Proxy ayarları kaydedildi - Cihazınızın proxy ayarlarını kullanır - Port 1–65535 arası olmalı - Doğrudan bağlantı, proxy yok Proxy ayarları kaydedilemedi Proxy ana bilgisayarı gerekli Geçerli bir ana bilgisayar adı veya IP adresi girin Geçersiz proxy portu - Şifreyi göster - Şifreyi gizle - Test - Test ediliyor… Bağlantı tamam (%1$d ms) Sunucu adresi çözümlenemedi. Proxy adresini kontrol edin. Proxy sunucusuna ulaşılamadı. @@ -435,20 +369,8 @@ Proxy kimlik doğrulaması gerekiyor. Beklenmeyen yanıt: HTTP %1$d Bağlantı testi başarısız - Her kategori kendi proxy'sini kullanabilir. Bunları bağımsız olarak yapılandırın. - Keşif (GitHub API) - Ana sayfa, arama, depo ayrıntıları ve güncelleme denetimleri - İndirmeler - APK indirmeleri ve otomatik güncellemeler - Çeviri - README çeviri hizmeti - - Bu uygulamayı izle - Uygulama izleme listesine eklendi - Uygulama izlenemedi: %1$s - Uygulama zaten izleniyor GitHub ile giriş yap @@ -465,7 +387,6 @@ Tekrar Giriş Yap Misafir olarak devam et Bu işlem yerel oturumunuzu ve önbellek verilerinizi temizleyecektir. Erişimi tamamen iptal etmek için GitHub Settings > Applications sayfasını ziyaret edin. - Kodun süresi %1$s sonra dolacak Cihaz kodunun süresi doldu. Yeni bir kod almak için lütfen tekrar giriş yapmayı deneyin. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin. @@ -482,27 +403,16 @@ Bağlantı paylaşılamadı Bağlantı panoya kopyalandı - - Çevir - Çevriliyor… - Orijinali göster - %1$s diline çevrildi Şuna çevir… Dil ara - Dili değiştir Çeviri başarısız. Lütfen tekrar deneyin. GitHub bağlantısını aç Panoda GitHub bağlantısı algılandı - Pano bağlantılarını otomatik algıla - Arama açılırken panodan GitHub bağlantılarını otomatik olarak algıla Algılanan bağlantılar Uygulamada aç Panoda GitHub bağlantısı bulunamadı - Depolama - Önbelleği Temizle - Geçerli boyut: Temizle @@ -520,8 +430,6 @@ - - Kurulum Varsayılan Standart sistem kurulum penceresi Shizuku @@ -538,7 +446,6 @@ Uygulamaları otomatik güncelle Shizuku aracılığıyla arka planda otomatik olarak güncellemeleri indirin ve yükleyin - Güncellemeler Güncelleme kontrol aralığı Arka planda uygulama güncellemelerinin ne sıklıkla kontrol edileceği Arka planda güncelleme kontrolü @@ -562,18 +469,13 @@ Doğrulanıyor… Bağla ve takip et Son sürüm kontrol ediliyor… - Doğrulama için APK indiriliyor… İmza anahtarı doğrulanıyor… Paket adı uyuşmuyor: APK %1$s, ancak seçilen uygulama %2$s İmza anahtarı uyuşmuyor: bu depodaki APK farklı bir geliştirici tarafından imzalanmış Yükleyici seçin Yüklü uygulamanızla doğrulamak için APK seçin - İndirme başarısız Dışa aktar İçe aktar - Uygulamaları içe aktar - Takip edilen uygulamaları geri yüklemek için dışa aktarılan JSON'u yapıştırın - Dışa aktarılan JSON'u buraya yapıştırın… Varsayılan beta kanalı Yeni takip edilen uygulamalar varsayılan olarak beta sürümleri içerir. Önceden takip edilen uygulamalar kendi ayarını korur (uygulamanın Ayrıntılar ekranından değiştirin). Uygulama kaldırılsın mı? @@ -586,9 +488,6 @@ %1$s, %2$s/%3$s ile bağlandı Dışa aktarma başarısız: %1$s İçe aktarma başarısız: %1$s - %1$d uygulama içe aktarıldı - , %1$d atlandı - , %1$d başarısız İmza anahtarı değişti Bu uygulamanın imza sertifikası ilk kurulumdan bu yana değişti.\n\nBu, geliştiricinin imza anahtarını değiştirdiği veya dosyanın değiştirilmiş olabileceği anlamına gelebilir.\n\nBeklenen: %1$s\nAlınan: %2$s Yine de yükle @@ -601,8 +500,6 @@ Birden fazla dosya mevcut Bu sürüm için birden fazla kurulabilir dosya mevcut. Listeyi inceleyin ve cihazınıza uygun olanı seçin. Bilgi - Tekrar dene - Otomatik algılanan: %1$s Dil seçin Paket uyumsuzluğu: APK %1$s, ancak yüklü uygulama %2$s. Güncelleme engellendi. İmza anahtarı uyumsuzluğu: güncelleme farklı bir geliştirici tarafından imzalanmış. Güncelleme engellendi. @@ -615,21 +512,13 @@ Geniş Çok geniş - Görülen depoları gizle - Zaten görüntülediğiniz depoları keşif akışlarından gizleyin - Görüntüleme geçmişini temizle - Tüm görüntülenen depoları sıfırlayarak akışlarda tekrar görünmelerini sağlayın Görüntüleme geçmişi temizlendi Görüntülendi Bu deponun sahibisin Depoyu gizle - Depo gizlendi - Geri al Son görüntülenenler Ziyaret ettiğiniz depolar - İndirilen paketler - GitHub sürümlerinden APK ve kurulum dosyaları Tümünü sil Tüm indirmeler silinsin mi? Bu işlem tüm APK ve kurulum dosyalarını kalıcı olarak silecek (%1$s). İstediğiniz zaman yeniden indirebilirsiniz. @@ -640,9 +529,7 @@ Tümünü temizle Kaldır - İnce Ayarlar İnce Ayarlar - Ön sürümler Varlık filtresi @@ -702,8 +589,6 @@ Yüklemeye hazır Yükle (hazır) - - Çeviri README çevirisi için kullanılacak hizmeti seçin. Çeviri Hizmeti Google yapılandırma gerektirmeden her yerde çalışır. Youdao Çin anakarasından çalışır ancak Youdao geliştirici portalından API kimlik bilgileri gerekir. @@ -746,8 +631,6 @@ ghp_… veya github_pat_… Giriş yap İptal - Token'ı göster - Token'ı gizle Lütfen bir token yapıştırın. Bu bir GitHub token'ına benzemiyor. Tokenlar ghp_ veya github_pat_ ile başlar. Bu token geçersiz veya iptal edilmiş. @@ -768,13 +651,11 @@ Sürüm kanalını seçin Bu uygulamanın kararlı sürümleri ile beta sürümleri arasında geçiş yapmak için dokunun. Anladım - Bu uygulama için beta sürümlerini aç/kapat %1$s kararlı sürümüne geç %1$d aydır kararlı sürüm yok %1$d gündür kararlı sürüm yok Etkin ön sürümler mevcut, ancak proje bir süredir kararlı bir derleme yayınlamadı. Beta sürümler kararlı bir sürüme ulaşmayabilir. %1$s sürümünden bu yana değişenler - — %1$s — Bu depodan bağlantıyı kes - Daha fazla seçenek Bu uygulamanın bağlantısı kesilsin mi? %1$s uygulamasını bu depodan yüklü olarak takip etmeyi bırakacağız. Uygulama cihazınızda kalır — yalnızca bağlantı kaldırılır. Bağlantıyı kes @@ -888,7 +767,6 @@ - Geri bildirim gönder Geri bildirim gönder Kapat Kategori @@ -926,15 +804,12 @@ ═══════════════════════════════════════════════════════════════ --> Özel forgeler - GHS'in tanımasını istediğin Forgejo / Gitea hostlarını ekle - %1$d eklendi Özel forgeler Bir Forgejo veya Gitea örneğinin host adını gir (örn. git.example.com). GHS, manuel bağlama sayfasında bu hostlardan URL kabul eder. Ekle Henüz özel forge yok. Bitti - İndirme Aynası İndirme Aynası Sürüm dosyalarını indirmek için kullanılır. GitHub API çağrıları her zaman doğrudan gider. Çoğu kullanıcı bunu Doğrudan GitHub olarak bırakmalıdır. Resmi @@ -950,7 +825,6 @@ Şablon {url} ifadesini tam olarak bir kez içermelidir Kaydet Seçileni test et - Test ediliyor… %1$dms'de ulaşıldı Ayna %1$d döndürdü 5s sonra zaman aşımı @@ -958,12 +832,6 @@ Başarısız: %1$s Tüm aynalar mı çöktü? 5 dakikada kendinizinkini barındırabilirsiniz — belgelere bakın. %1$s artık kullanılamıyor, Doğrudan GitHub'a geçildi. - Sağlama toplamı uyumsuzluğu — dosya değiştirilmiş olabilir - Daha hızlı bir ayna deneyin mi? - Yavaş ağlardaki bazı kullanıcılar topluluk proxy'siyle daha iyi sonuç alıyor. - Birini seç - Belki daha sonra - Bir daha sorma Yıldızlılardan ekle @@ -974,13 +842,7 @@ Güvenlik Durum Anketler - Onayladıktan sonra kapat - %1$d g Şu anda paylaşılacak bir şey yok. - %1$d sa - az önce - %1$s önce yenilendi - %1$d dk Devre dışı bırakılamaz Şuradan öğeleri göster Sessize alma ayarlarını aç @@ -1037,7 +899,6 @@ Daraltılmış Genişletilmiş Güncel - Güncellemeler mevcut %1$s için park edilmiş yükleyiciyi silip uygulama listenizden satırı kaldırmak istiyor musunuz? İndirilen APK silinecek. Bekleyen kurulumu sil? İzin ver @@ -1123,7 +984,6 @@ Yeni %1$s sürümünde neler yeni Neler yeni - Changelog İngilizcedir. Çeviriler Issues üzerinden memnuniyetle karşılanır. Sürüm %1$s · %2$s @@ -1161,7 +1021,6 @@ Windows macOS Linux - Diğer platform — aktarma için kaydetmek üzere tarayıcıda açılır Cihazınız Aktarım için Android'i Açık Tutun diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 03cc7d2c4..57ed23268 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1,14 +1,12 @@ GitHub Store - 已安装的应用 返回 检查更新 无法启动 %1$s 无法打开 %1$s 更新 %1$s 失败:%2$s - 更新失败 全部更新失败:%1$s 所有应用已成功更新 暂无可用更新 @@ -50,12 +48,9 @@ 使用 GitHub 登录 - 已取消 未知错误 - 语言: - 发现仓库 搜索仓库、描述… 按语言筛选 @@ -100,29 +95,18 @@ 个人资料 - 外观 - 语言 - 覆盖应用界面语言。 - 应用语言 - 更改应用中的菜单、按钮和消息。不会更改来自 GitHub 的内容。 跟随系统 重新启动以应用新语言。 重新启动 - 关于 - 网络 主题颜色 AMOLED 黑色主题 深色模式下的纯黑背景 - 已选择颜色:%1$s - 版本 - 帮助与支持 退出登录 已成功退出,正在跳转… - 缓存已成功清除 警告! 确定要退出登录吗? @@ -132,16 +116,13 @@ Cream Plum - 加载详情失败 关于此应用 安装日志 - 作者 更新内容 已安装 有可用更新 安装最新版本 - 重新安装 正在下载 正在更新 @@ -160,13 +141,10 @@ GitHub 上没有更多结果 从 GitHub 获取失败,请重试。 - 由 %1$s - • 已安装:%1$s 架构兼容 更新到 %1$s 报告问题 - 无法加载详情 安装程序已保存到下载文件夹 开始下载 @@ -189,30 +167,13 @@ 使用第三方应用安装APK 错误:%1$s - 不支持的文件类型 .%1$s - 未找到下载的文件 - 热门 - 热门发布 - 最受欢迎 - 隐私 - 媒体 - 生产力 - 网络 - 开发工具 正在查找仓库… - 加载中… - 没有更多仓库了 重试 加载仓库失败 - 查看详情 - 刚刚更新 - %1$d 小时前更新 - 昨天更新 - %1$d 天前更新 %1$s 更新 请求次数已达上限 @@ -261,8 +222,6 @@ 关闭 同步收藏的仓库失败 - 开发者简介 - 打开开发者个人资料 加载仓库失败 @@ -276,7 +235,6 @@ 搜索仓库… 清除搜索 - 全部 有发布版 已安装 收藏 @@ -288,7 +246,6 @@ 个仓库 - 个仓库 显示 %2$d 个中的 %1$d 个仓库 @@ -296,8 +253,6 @@ 没有已安装的仓库 没有收藏的仓库 - - %1$s前更新 有发布版 @@ -344,7 +299,6 @@ 不可用 - 更新应用 等待安装 @@ -368,35 +322,21 @@ 上次检查:%1$s - 从未检查 刚刚 %1$d 分钟前 %1$d 小时前 正在检查更新… - - 代理类型 - - 系统代理 - HTTP - SOCKS 主机地址 端口 用户名(可选) 密码(可选) 保存代理 代理设置已保存 - 使用设备的代理设置 - 端口必须为 1–65535 - 直连,不使用代理 无法保存代理设置 必须填写代理主机 请输入有效的主机名或 IP 地址 无效的代理端口 - 显示密码 - 隐藏密码 - 测试 - 测试中… 连接正常 (%1$d ms) 无法解析主机。请检查代理地址。 无法连接到代理服务器。 @@ -404,20 +344,8 @@ 需要代理身份验证。 意外响应:HTTP %1$d 连接测试失败 - 每个类别都可以使用自己的代理。请独立配置它们。 - 发现 (GitHub API) - 主页、搜索、仓库详情和更新检查 - 下载 - APK 下载和自动更新 - 翻译 - README 翻译服务 - - 跟踪此应用 - 应用已添加到跟踪列表 - 跟踪应用失败:%1$s - 该应用已在跟踪中 登录 GitHub @@ -434,7 +362,6 @@ 重新登录 以访客身份继续 这将清除您的本地会话和缓存数据。要完全撤销访问权限,请访问 GitHub Settings > Applications。 - 验证码将在 %1$s 后过期 设备验证码已过期。 请重新登录以获取新的验证码。 请检查您的网络连接并重试。 @@ -451,27 +378,16 @@ 无法分享链接 链接已复制到剪贴板 - - 翻译 - 翻译中… - 显示原文 - 已翻译为%1$s 翻译为… 搜索语言 - 更改语言 翻译失败,请重试。 打开 GitHub 链接 在剪贴板中检测到 GitHub 链接 - 自动检测剪贴板链接 - 打开搜索时自动检测剪贴板中的 GitHub 链接 检测到的链接 在应用中打开 剪贴板中未找到 GitHub 链接 - 存储 - 清除缓存 - 当前大小: 清除 @@ -486,8 +402,6 @@ - - 安装 默认 标准系统安装对话框 Shizuku @@ -504,7 +418,6 @@ 自动更新应用 通过 Shizuku 在后台自动下载并安装更新 - 更新 更新检查间隔 在后台检查应用更新的频率 后台检查更新 @@ -528,18 +441,13 @@ 验证中… 链接并追踪 检查最新版本… - 正在下载APK进行验证… 正在验证签名密钥… 包名不匹配:APK为%1$s,但所选应用为%2$s 签名密钥不匹配:此仓库中的APK由不同的开发者签名 选择安装包 选择APK以与已安装的应用进行验证 - 下载失败 导出 导入 - 导入应用 - 粘贴导出的JSON以恢复跟踪的应用 - 在此粘贴导出的JSON… 默认测试版渠道 新追踪的应用默认包含测试版。已追踪的应用保留各自设置(在应用详情页切换)。 卸载应用? @@ -552,9 +460,6 @@ %1$s已链接到%2$s/%3$s 导出失败:%1$s 导入失败:%1$s - 已导入%1$d个应用 - ,%1$d个已跳过 - ,%1$d个失败 签名密钥已更改 此应用的签名证书自首次安装以来已更改。\n\n这可能意味着开发者更换了签名密钥,或者二进制文件可能已被篡改。\n\n预期:%1$s\n收到:%2$s 仍然安装 @@ -567,8 +472,6 @@ 多个资源可用 此版本有多个可安装文件。请查看列表并选择适合您设备的文件。 信息 - 重试 - 自动检测:%1$s 选择语言 包名不匹配:APK 为 %1$s,但已安装的应用为 %2$s。更新已阻止。 签名密钥不匹配:更新由不同的开发者签名。更新已阻止。 @@ -581,21 +484,13 @@ 超宽 - 隐藏已浏览的仓库 - 在发现信息流中隐藏你已经浏览过的仓库 - 清除浏览记录 - 重置所有已浏览的仓库,使其重新出现在信息流中 浏览记录已清除 已浏览 你拥有此仓库 隐藏仓库 - 仓库已隐藏 - 撤销 最近查看 你访问过的仓库 - 已下载的包 - 来自 GitHub 发布的 APK 和安装程序 全部删除 删除所有下载? 这将永久删除所有 APK 和安装程序(%1$s)。你可以随时重新下载。 @@ -606,9 +501,7 @@ 清除全部 移除 - 调整 调整 - 预发布版本 资产过滤器 @@ -666,8 +559,6 @@ 准备安装 安装(已就绪) - - 翻译 选择用于翻译 README 的服务。 翻译服务 Google 全球可用,无需配置。有道可从中国大陆访问,但需要从有道开发者门户获取 API 凭据。 @@ -710,8 +601,6 @@ ghp_… 或 github_pat_… 登录 取消 - 显示令牌 - 隐藏令牌 请粘贴令牌。 这不像是 GitHub 令牌。令牌以 ghp_ 或 github_pat_ 开头。 该令牌无效或已被吊销。 @@ -733,13 +622,11 @@ 选择发布渠道 点按以在此应用的稳定版和测试版之间切换。 知道了 - 切换此应用的测试版发布 切换到 %1$s 稳定版 %1$d 个月内无稳定版发布 %1$d 天内无稳定版发布 目前有活跃的预发布版本,但该项目已有一段时间未发布稳定版。测试版可能不会收敛为稳定版。 自 %1$s 以来的变更 - — %1$s — 从此仓库取消关联 - 更多选项 取消关联此应用? 我们将停止追踪 %1$s 作为从此仓库安装的应用。该应用仍保留在您的设备上 — 仅删除关联。 取消关联 @@ -848,7 +733,6 @@ - 发送反馈 发送反馈 关闭 类别 @@ -886,15 +770,12 @@ ═══════════════════════════════════════════════════════════════ --> 自定义代码托管 - 添加你希望 GHS 识别的 Forgejo / Gitea 主机 - 已添加 %1$d 个 自定义代码托管 输入 Forgejo 或 Gitea 实例的主机名(例如 git.example.com)。在手动关联表单中,GHS 将接受这些主机的链接。 添加 尚无自定义代码托管。 完成 - 下载镜像 下载镜像 用于下载发布资源。GitHub API 调用始终直接进行。大多数用户应保持"直连 GitHub"设置。 官方 @@ -910,7 +791,6 @@ 模板必须恰好包含一次 {url} 保存 测试所选 - 测试中… 已在 %1$dms 内到达 镜像返回 %1$d 5s 后超时 @@ -918,12 +798,6 @@ 失败:%1$s 所有镜像都无法使用?您可以在 5 分钟内自行托管 — 请参阅文档。 %1$s 已不再可用,已切换到直连 GitHub。 - 校验和不匹配 — 文件可能已被篡改 - 尝试更快的镜像? - 网络较慢的部分用户使用社区代理效果更好。 - 选择一个 - 稍后再说 - 不再询问 从星标添加 @@ -934,13 +808,7 @@ 安全 状态 调查 - 确认后关闭 - %1$d 天 当前没有可分享的内容。 - %1$d 小时 - 刚刚 - %1$s 前刷新 - %1$d 分钟 无法关闭 显示来自 打开静音设置 @@ -997,7 +865,6 @@ 已折叠 已展开 已是最新 - 有可用更新 放弃 %1$s 的暂存安装包并从应用列表中移除该行?已下载的 APK 将被删除。 放弃待安装项? 授予权限 @@ -1083,7 +950,6 @@ 新增 %1$s 的更新内容 更新内容 - 更新日志为英文。欢迎通过 Issues 提交翻译。 版本 %1$s · %2$s @@ -1121,7 +987,6 @@ Windows macOS Linux - 其他平台 — 在浏览器中打开以保存并转移 你的设备 用于转移 保持 Android 开放 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 875bbfbf2..83dc0c5b8 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -3,8 +3,6 @@ GitHub Store - - Installed Apps Navigate back Check for updates @@ -12,7 +10,6 @@ Cannot launch %1$s Failed to open %1$s Failed to update %1$s: %2$s - Update failed Update all failed: %1$s All apps updated successfully No updates available @@ -64,12 +61,9 @@ Sign in with GitHub - Cancelled Unknown error - Language: - Discover Repositories Search repo, description… @@ -118,54 +112,29 @@ Profile - - Appearance - Language - Override the app's UI language. - App language - Changes menus, buttons, and messages throughout the app. Does not change content coming from GitHub. Follow system Restart to apply the new language. Restart - Network - About Palette True black (AMOLED) Pure-black background — saves power on OLED screens. - Selected color: %1$s - - Version - Help & Support Logout - - Proxy Type - No proxy - System - HTTP/HTTPS - SOCKS5 Host Port Username (optional) Password (optional) Save Proxy settings saved - Uses your device's proxy settings - Port must be 1–65535 - The app connects to the internet without a proxy. Failed to save proxy settings Proxy host is required Enter a valid hostname or IP address Invalid proxy port - Show password - Hide password - Test - Testing… Connection OK (%1$d ms) Could not resolve host. Check the proxy address. Could not reach the proxy server. @@ -173,13 +142,6 @@ Proxy authentication required. Unexpected response: HTTP %1$d Connection test failed - Each category can use its own proxy. Configure them independently. - Search & metadata - Home, search, repo details, and update checks - Downloads - APK downloads and auto-updates - Translation - README translation service Logged out successfully, redirecting... @@ -200,8 +162,6 @@ Cancel download Show install options - - Error loading details Something went wrong You're offline Rate limit hit @@ -218,7 +178,6 @@ About this app Install logs - Author What’s New @@ -248,8 +207,6 @@ ghp_… or github_pat_… Sign in Cancel - Show token - Hide token Please paste a token. That doesn't look like a GitHub token. Tokens start with ghp_ or github_pat_. That token is invalid or has been revoked. @@ -260,8 +217,6 @@ No releases published yet Loading releases… Install latest - Reinstall - Update app Report issue @@ -311,14 +266,9 @@ No more results on GitHub Failed to fetch from GitHub. Try again. - - by %1$s - • Installed: %1$s Architecture compatible Update to %1$s - - Failed to load details Installer was saved into the Downloads folder @@ -346,32 +296,18 @@ Error: %1$s - Asset type .%1$s not supported - Downloaded file not found - Trending - Hot release - Most popular - Privacy - Media - Productivity - Network - Dev Tools Finding repositories... - Loading more... - No more repositories Retry Failed to load repositories - View Details Discover Hot releases Trending now Most popular From your stars - HOT · %1$s ago #%1$d Get Back @@ -380,24 +316,12 @@ View all View more - Update available: %1$s → %2$s - What's changed since %1$s - via %1$s - sha-256 verified - change - View all %1$d releases - View all releases View profile Developer - Permissions Stats About What's new - updated just now - updated %1$d hour(s) ago - updated yesterday - updated %1$d day(s) ago updated on %1$s Rate Limit Exceeded @@ -451,8 +375,6 @@ Dismiss Failed to sync starred repos - Developer Profile - Open developer profile Failed to load repositories Failed to load profile @@ -465,7 +387,6 @@ Search repositories… Clear search - All With Releases Installed Favorites @@ -477,7 +398,6 @@ repository - repositories Showing %1$d of %2$d repositories @@ -485,8 +405,6 @@ No installed repositories No favorite repositories - - Updated %1$s Has Release %1$d y ago @@ -509,7 +427,6 @@ Search Library Profile - Tweaks Fork @@ -527,13 +444,11 @@ Choose your release channel Tap to switch between stable releases and beta builds for this app. Got it - Toggle beta releases for this app Switch to stable %1$s No stable release in %1$d months No stable release in %1$d days Active pre-releases but the project hasn't shipped a stable build in a while. Betas may not converge to a stable release. What's changed since %1$s - — %1$s — No version Versions Assets @@ -547,17 +462,11 @@ Last checked: %1$s - Never checked just now %1$d min ago %1$d h ago Checking for updates… - - Track this app - App added to tracking list - Failed to track app: %1$s - App is already being tracked Sign in to GitHub @@ -577,8 +486,6 @@ This will clear your local session and cached data. To fully revoke access, visit GitHub Settings > Applications. - - Code expires in %1$s The device code has expired. Please try signing in again to get a new code. Please check your internet connection and try again. @@ -595,40 +502,23 @@ Failed to share link Link copied to clipboard - - Translate - Translating… - Show original - Translated to %1$s Translate to… Search language - Change language Translation failed. Please try again. - Retry - Auto-detected: %1$s Select language Open GitHub Link GitHub link detected in clipboard - Detect repo links in clipboard - When you copy a github.com or codeberg.org link, we\'ll prompt to open it. Detected Links Open in app No GitHub link found in clipboard - Storage - Downloaded APKs - We keep installers around so updates resume fast. - Using: Delete All Delete all downloads? This will permanently remove all downloaded APKs and installers (%1$s). You can re-download them anytime. All downloaded packages have been deleted. - - Clear cache Clear - The cache is gradually cleared. @@ -642,8 +532,6 @@ - - Installation System installer Asks each time. Works on every device. Shizuku @@ -687,7 +575,6 @@ Use a valid Android package name (e.g. com.example.installer) Some apps detect when their installer changes and may refuse to run, or fail security checks (e.g. Play Integrity, banking apps). - Updates Check every How often to look for new releases. Check automatically @@ -711,13 +598,11 @@ Validating… Link & Track Checking latest release… - Downloading APK for verification… Verifying signing key… Package name mismatch: the APK is %1$s, but the selected app is %2$s Signing key mismatch: the APK in this repository was signed by a different developer than the installed app Select installer Pick the APK to verify against your installed app - Download failed Export library Export for Obtainium Import from file @@ -753,9 +638,6 @@ GitHub rate limit reached. Resume to keep checking. Resume Removing a star on GitHub won't untrack the app. - Import apps - Paste the exported JSON to restore your tracked apps - Paste exported JSON here… Default beta channel Newly tracked apps include beta builds by default. Already-tracked apps keep their own per-app channel setting (toggle it on the app's Details screen). @@ -772,9 +654,6 @@ %1$s linked to %2$s/%3$s Export failed: %1$s Import failed: %1$s - Imported %1$d apps - , %1$d skipped - , %1$d failed Package mismatch: the APK is %1$s, but the installed app is %2$s. Update blocked. Signing key mismatch: the update was signed by a different developer. Update blocked. @@ -788,10 +667,6 @@ Wide Extra wide - Hide repos I\'ve already viewed - Skip seen repos in feeds and search. - Clear viewed history - Forget which repos you\'ve already opened. Seen history cleared Viewed You own this repo @@ -804,11 +679,8 @@ Windows macOS Linux - Other platform — opens in browser to save for transfer Your device For transfer - Repository hidden - Undo Hidden repositories Manage repositories you hid from Home and Search. %1$d hidden @@ -827,8 +699,6 @@ Tweaks - - Pre-releases Asset filter @@ -906,8 +776,6 @@ Ready to install Install (ready) - - Translation Choose the service used to translate README content. Translation Service Google works globally without configuration. Youdao works from mainland China but requires API credentials from Youdao's developer portal. @@ -955,7 +823,6 @@ Skip Link Installed via %1$s - We think this is %1$s · %2$d We think this is Tap to find a repo by %1$s @@ -1028,7 +895,6 @@ Unlink from this repo - More options Unlink this app? We'll stop tracking %1$s as installed from this repo. The app stays on your device — only the link is removed. Unlink @@ -1048,8 +914,6 @@ Couldn't refresh. Try again shortly. Refresh failed. Try again. - - Send feedback Send feedback Close @@ -1094,8 +958,6 @@ Custom forges - Add Forgejo / Gitea hosts you want GHS to recognise - %1$d added Custom forges Enter the hostname of a Forgejo or Gitea instance (e.g. git.example.com). GHS will accept URLs from these hosts in the manual-link sheet. Codeberg, gitea.com and git.disroot.org are already searched by default — you don't need to add them here. Use this list only for your own self-hosted Forgejo or Gitea. @@ -1103,8 +965,6 @@ No custom forges yet. Done - - GitHub mirror Download Mirror Used for downloading release assets. GitHub API calls always go direct. Most users should leave this on Direct GitHub. @@ -1128,7 +988,6 @@ Test selected - Testing… Reached in %1$dms Mirror returned %1$d Timed out after 5s @@ -1140,17 +999,8 @@ %1$s is no longer available, switched to Direct GitHub. - Checksum mismatch — file may have been tampered with - - Try a faster mirror? - Some users on slow networks have better luck with a community proxy. - Pick one - Maybe later - Don't ask again - - Updates available Up to date · %1$d Expand section @@ -1192,7 +1042,6 @@ Showing original Target language Translate to %1$s - Translate Show original Show translation Cancel @@ -1207,7 +1056,6 @@ Sort by Reset all Done - Active Remove filter App settings, theme, network, translation, advanced options. @@ -1240,7 +1088,6 @@ usage data Restart now Later - Android only Install method is Android-only Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager. Coming in a follow-up @@ -1292,18 +1139,12 @@ Heads up Got it View previous versions - Changelog is in English. Translations welcome via Issues. No changelog entries yet. Highlights from recent releases. Announcements News, surveys, and important notices. Nothing to share right now. - Last refreshed %1$s ago Couldn't refresh — showing cached items. - just now - %1$d min - %1$d h - %1$d d I've read this Acknowledged Read more @@ -1318,7 +1159,6 @@ Info Important Critical - Close after acknowledgment Open mute settings @@ -1346,19 +1186,14 @@ Auto-translate repositories Translate README and release notes automatically when opening a repository. Uses your app language as the target. - Target: %1$s - Reset Translate into Following app language (%1$s) Authentication tokens Personal access tokens per forge host (GitHub, Codeberg, Forgejo) - Personal access tokens stored encrypted per forge host. Used to authenticate direct API calls. No tokens stored Tap + to add a personal access token - Add token - Host (e.g. github.com) Personal access token Display name (optional) Host required @@ -1369,11 +1204,8 @@ Cancel Back Add token - Validate Delete Saved token for %1$s - Removed token for %1$s - Token accepted (login=%1$s, scopes=%2$s) Validation failed: %1$s diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 676d228df..74b7ea1a9 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -77,7 +77,6 @@ import zed.rainxch.githubstore.core.presentation.res.hide_repository import zed.rainxch.githubstore.core.presentation.res.mark_as_unviewed import zed.rainxch.githubstore.core.presentation.res.mark_as_viewed import zed.rainxch.githubstore.core.presentation.res.open_on_github -import zed.rainxch.githubstore.core.presentation.res.home_view_details import zed.rainxch.githubstore.core.presentation.res.installed import zed.rainxch.githubstore.core.presentation.res.open_in_browser import zed.rainxch.githubstore.core.presentation.res.seen_badge diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt index ae4a26c29..9112161c6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewSheet.kt @@ -38,7 +38,6 @@ import zed.rainxch.githubstore.core.presentation.res.whats_new_section_heads_up import zed.rainxch.githubstore.core.presentation.res.whats_new_section_improved import zed.rainxch.githubstore.core.presentation.res.whats_new_section_new import zed.rainxch.githubstore.core.presentation.res.whats_new_sheet_heading -import zed.rainxch.githubstore.core.presentation.res.whats_new_translations_note import zed.rainxch.githubstore.core.presentation.res.whats_new_version_label @OptIn(ExperimentalMaterial3Api::class) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index 6cd917d5f..7700b0843 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -141,7 +141,6 @@ import zed.rainxch.githubstore.core.presentation.res.export_apps_obtainium import zed.rainxch.githubstore.core.presentation.res.external_import_rescan_menu import zed.rainxch.githubstore.core.presentation.res.import_apps import zed.rainxch.githubstore.core.presentation.res.bottom_nav_apps_title -import zed.rainxch.githubstore.core.presentation.res.installed_apps import zed.rainxch.githubstore.core.presentation.res.installing import zed.rainxch.githubstore.core.presentation.res.last_checked import zed.rainxch.githubstore.core.presentation.res.last_checked_hours_ago diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt index fc8a990a8..ea9857f86 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/language/TweaksLanguageRoot.kt @@ -40,7 +40,6 @@ import zed.rainxch.core.domain.model.AppLanguages import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.language_follow_system -import zed.rainxch.githubstore.core.presentation.res.language_picker_title import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_language import zed.rainxch.githubstore.core.presentation.res.tweaks_language_intro_body import zed.rainxch.tweaks.presentation.TweaksAction From 0d433bc64f105bb6a7d7291b90002d47dd961332 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 16:05:11 +0500 Subject: [PATCH 122/172] i18n: translate 12 locales to 100% parity --- .../composeResources/values-ar/strings-ar.xml | 262 ++++++++++++++++++ .../composeResources/values-bn/strings-bn.xml | 254 +++++++++++++++++ .../composeResources/values-es/strings-es.xml | 250 +++++++++++++++++ .../composeResources/values-fr/strings-fr.xml | 249 +++++++++++++++++ .../composeResources/values-hi/strings-hi.xml | 258 +++++++++++++++++ .../composeResources/values-it/strings-it.xml | 250 +++++++++++++++++ .../composeResources/values-ja/strings-ja.xml | 246 ++++++++++++++++ .../composeResources/values-ko/strings-ko.xml | 247 +++++++++++++++++ .../composeResources/values-pl/strings-pl.xml | 256 +++++++++++++++++ .../composeResources/values-ru/strings-ru.xml | 256 +++++++++++++++++ .../composeResources/values-tr/strings-tr.xml | 254 +++++++++++++++++ .../values-zh-rCN/strings-zh-rCN.xml | 247 +++++++++++++++++ 12 files changed, 3029 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index bfdd64a65..2d2110be0 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1039,4 +1039,266 @@ سياسة التحقق من المطورين من Google تهدد التوزيع المفتوح لأندرويد. GitHub Store يدعم التحالف. اعرف المزيد إغلاق البانر + + + انقر على أي بطاقة على اليسار لعرض تفاصيلها هنا. + اختر مستودعاً + جاهز للتثبيت + إعدادات متقدمة + إجراءات سريعة + الإعدادات + الحالة + اختر أي تطبيق على اليسار لرؤية تفاصيله وإجراءاته. + اختر تطبيقاً + المثبَّت + الأحدث + فتح المستودع + اختيار النوع + إخفاء القائمة + عرض القائمة + راجع أو حدّث كل شيء دفعة واحدة. + تحديث واحد متاح + %1$d تحديثات متاحة + رجوع + رجوع + صورة + عرض الكل › + + لا توجد مضيفات + مضيف واحد + مضيفان + %1$d مضيفات + %1$d مضيفاً + %1$d مضيف + + يتم البحث في Codeberg و gitea.com و git.disroot.org افتراضياً — لا حاجة لإضافتها هنا. استخدم هذه القائمة لمضيف Forgejo أو Gitea الذي تستضيفه بنفسك فقط. + حول + المطوّر + الإحصائيات + عرض الملف الشخصي + ما الجديد + تخطّي والمراجعة لاحقاً + نقرأ كل تقرير. + الحصول + #%1$d + من مستودعاتك المميّزة + إصدارات رائجة + الأكثر شعبية + الأعلى تداولاً الآن + استكشف + عرض الكل + عرض المزيد + إضافة رمز + رجوع + إلغاء + حذف + حفظ + إضافة رمز + تم الاكتشاف: %1$s + عنوان المنصّة + اتركه فارغاً للاحتفاظ بالرمز الحالي. اكتب قيمة جديدة لاستبداله. + استبدال الرمز لـ %1$s + سيتم الاتصال بـ %1$s + + لا توجد رموز + رمز واحد + رمزان + %1$d رموز + %1$d رمزاً + %1$d رمز + + انقر على + لإضافة رمز وصول شخصي + لا توجد رموز محفوظة + رموز الوصول الشخصية لكل مضيف منصّة (GitHub، Codeberg، Forgejo) + اسم العرض (اختياري) + رمز الوصول الشخصي + أنت مسجَّل الدخول إلى GitHub عبر التطبيق — وهذا هو المسار الموصى به. أضف رمزاً هنا فقط إذا تعذّر تسجيل الدخول عبر المتصفح (شبكة مقيّدة، لا متصفح) أو لمنصّات غير GitHub. + فتح صفحة الرمز + منصّة أخرى + Forgejo، Gitea، استضافة ذاتية + لصق الرمز + إضافة رمز لـ… + تعديل التسمية + غير صالح · %1$s + المزيد + الإدارة على %1$s + استبدال الرمز + %1$s متبقٍ + صالح · %1$s + تحقّق + تم حفظ الرمز لـ %1$s + رموز المصادقة + تراجع + تعذّر استعادة الرمز لـ %1$s + تمت إزالة الرمز لـ %1$s + فشل التحقق: %1$s + خطأ غير معروف + المضيف غير صالح + المضيف مطلوب + الرمز مطلوب + الرمز قصير جداً + حول GitHub Store + إرسال ملاحظات… + تراخيص المصادر المفتوحة + مساعدة + سياسة الخصوصية + إعدادات التطبيق، السمة، الشبكة، الترجمة، الخيارات المتقدمة. + تتطلب بعض التغييرات إعادة تشغيل التطبيق لتفعيلها. + لاحقاً + اللغة + بيانات الاستخدام + السمة + المتأثر: %1$s + إعادة التشغيل الآن + إزالة الفلتر + تم + فلاتر + إعادة تعيين الكل + اللغة + المنصّة + ترتيب حسب + المصدر + تصفية النتائج + التطبيق + الاتصال + التثبيت والتحديثات + المظهر + الخصوصية والبيانات + + لا توجد تطبيقات + تطبيق واحد + تطبيقان + %1$d تطبيقات + %1$d تطبيقاً + %1$d تطبيق + + ترجمة README وملاحظات الإصدار تلقائياً عند فتح مستودع. تستخدم لغة التطبيق كهدف. + اتباع لغة التطبيق (%1$s) + الترجمة إلى + ترجمة المستودعات تلقائياً + إلغاء + تغيير اللغة + المصدر المكتشف: %1$s + إعادة المحاولة + عرض النص الأصلي + عرض الترجمة + اعرض هذه الصفحة بلغة أخرى. + عرض النص الأصلي + تمت الترجمة إلى %1$s + جارٍ الترجمة… + اللغة الهدف + ترجمة + الترجمة إلى %1$s + مفتاح المصادقة + احصل على مفتاح DeepL مجاني ← + سجّل في deepl.com/pro-api. مفاتيح الطبقة المجانية تنتهي بـ :fx وتستخدم نقطة النهاية المجانية تلقائياً. الطبقة المدفوعة تحافظ على خصوصية نصك؛ الطبقة المجانية قد تستخدم النص المرسَل لتحسين النماذج. + حفظ المفتاح + تم حفظ مفتاح DeepL + مفتاح API (اختياري) + عنوان المثيل + يعمل مباشرةً عبر مرآة Disroot العامة — اترك الحقول فارغة لاستخدامها. لخصوصية أكبر، الصق رابط استضافتك الذاتية. لا يلزم مفتاح API إلا إذا اشترطه المثيل. + حفظ الإعدادات + تم حفظ إعدادات LibreTranslate + احصل على مفتاح Azure مجاني ← + Azure Translator يحترم الخصوصية (وضع No-Trace افتراضياً — لا يُخزَّن نصك ولا يُستخدم للتدريب). الطبقة المجانية تغطي مليوني حرف شهرياً. المنطقة لازمة فقط للموارد غير العالمية. + مفتاح الاشتراك + المنطقة (مثل westeurope، اتركه فارغاً للعالمي) + حفظ بيانات الاعتماد + تم حفظ بيانات اعتماد Microsoft Translator + DeepL + LibreTranslate + Microsoft + GitHub Store + المكتبات المستخدمة في التطبيق. + تراخيص المصادر المفتوحة + العرض على github-store.org. + سياسة الخصوصية + عرض كود هذا التطبيق. + الكود المصدري على GitHub + متجر تطبيقات عابر للمنصّات لإصدارات GitHub و Codeberg و Forgejo. + ملاحظات الإصدارات السابقة. + ما الجديد + يُطبَّق على كل الحركة ما لم يُعَد تجاوزه أدناه. + مخصّص + اختر وضع الاتصال أدناه. معظم المستخدمين يتركونه على \'بدون وكيل\'. + كيف يصل التطبيق إلى الإنترنت + الاتصال الرئيسي + HTTP/HTTPS + معظم الوكلاء المؤسّسية. + بدون وكيل + SOCKS5 + Tor، أنفاق SSH. + النظام + يستخدم كل نطاق الاتصال الرئيسي افتراضياً. اختر \'مخصّص\' للاحتفاظ بإعداداته الخاصة. + تجاوزات لكل نطاق + لصق رابط كامل + الصق رابط وكيل كاملاً وسنملأ النموذج نيابةً عنك. + استخدام هذا الرابط + تعذّر قراءة هذا الرابط. + scheme://user:pass@host:port + لصق رابط الوكيل + واجهة GitHub البرمجية، البحث، تفاصيل المستودع. + البحث والبيانات الوصفية + تنزيلات APK والأصول. + التنزيلات + مكالمات DeepL و Microsoft و LibreTranslate. + الترجمة + اختبار + استخدام الرئيسي + رموز الوصول + معلومات التطبيق + المظهر + الاتصال + إرسال ملاحظات + طريقة التثبيت + اللغة + الخصوصية + المصادر + التخزين + انقر للإدارة + الترجمة + سلوك التحديث + المثبّتات الصامتة وإسناد المثبِّت ميزات خاصة بـ Android. عمليات التثبيت على سطح المكتب تمر عبر مدير حزم النظام. + طريقة التثبيت لـ Android فقط + يُعاد تشغيل التطبيق عند تبديل اللغة. + انقر على أي إدخال لفتح صفحة مشروعه. + يقوم GitHub Store على هذه المكتبات. + تراخيص المصادر المفتوحة + سجل التصفّح + انسَ المستودعات التي فتحتها سابقاً. + لن يُلغي هذا تمييز أي مستودع بنجمة أو إضافته للمفضلة. + مسح + مسح سجل المشاهدة؟ + مسح سجل المشاهدة + عند نسخك لرابط من github.com أو codeberg.org، سنطلب منك فتحه. + اكتشاف روابط المستودعات في الحافظة + المستودعات التي كتمتها من الموجَزات والبحث. + المستودعات المخفية + تخطّي المستودعات المُشاهَدة في الموجَزات والبحث. + إخفاء المستودعات التي شاهدتها بالفعل + ساعدنا في فهم الميزات التي يتم استخدامها. + إصدار التطبيق. + عدد مرات استخدام الميزات. + لا معرّفات. + لا أسماء مستودعات. + لا رموز. + نظام التشغيل والمنصّة. + مشاركة بيانات استخدام مجهولة + ما الذي نجمعه + بيانات الاستخدام + لا توجد إعدادات تطابق \'%1$s\'. + البحث في الإعدادات + إضافة مضيف Forgejo أو Gitea + المضيفات المضافة + مرآة GitHub + يبحث التطبيق في GitHub افتراضياً. وجِّه عبر مرآة إقليمية أو أضف مضيفات Forgejo / Gitea مخصّصة. + حيث يبحث التطبيق عن المستودعات + الافتراضي (github.com) + نحتفظ بالمثبّتات لتُستأنف التحديثات بسرعة. + مسح + ملفات APK المنزَّلة + 0 ب + يستخدم: %1$s + هذه الشاشة جزء من التصميم الجديد قيد التطوير. الإعدادات لا تزال تعمل — يجري نقلها إلى هنا من التخطيط القديم. + قادم في تحديث لاحق diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 3dcec83ee..ac4af39ce 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1017,4 +1017,258 @@ Google-এর ডেভেলপার-যাচাইকরণ নীতি উন্মুক্ত Android বিতরণকে হুমকির মুখে ফেলেছে। GitHub Store কোয়ালিশনের পাশে। আরও জানুন ব্যানার বন্ধ করুন + + + + %1$dটি হোস্ট + %1$dটি হোস্ট + + + %1$dটি রিপো + %1$dটি রিপো + + + %1$dটি টোকেন + %1$dটি টোকেন + + + %1$dটি অ্যাপ + %1$dটি অ্যাপ + + এখানে এর বিস্তারিত দেখতে বাঁদিকের যেকোনো কার্ডে আলতো চাপুন। + একটি রিপোজিটরি বেছে নিন + ইনস্টলের জন্য প্রস্তুত + উন্নত সেটিংস + দ্রুত অ্যাকশন + সেটিংস + অবস্থা + বাঁদিক থেকে যেকোনো অ্যাপ নির্বাচন করুন এর বিবরণ ও অ্যাকশন দেখতে। + একটি অ্যাপ বেছে নিন + ইনস্টল করা + সর্বশেষ + রিপোজিটরি খুলুন + ভ্যারিয়েন্ট বেছে নিন + তালিকা লুকান + তালিকা দেখুন + পর্যালোচনা করুন বা একসাথে সব আপডেট করুন। + ১টি আপডেট উপলব্ধ + %1$dটি আপডেট উপলব্ধ + পেছনে + পেছনে + ছবি + সব দেখুন › + Codeberg, gitea.com এবং git.disroot.org ইতিমধ্যেই ডিফল্টভাবে অনুসন্ধান করা হয় — সেগুলো এখানে যোগ করার দরকার নেই। শুধু আপনার নিজস্ব সেলফ-হোস্টেড Forgejo বা Gitea-এর জন্য এই তালিকা ব্যবহার করুন। + বৃত্তান্ত + ডেভেলপার + পরিসংখ্যান + প্রোফাইল দেখুন + নতুন কী আছে + বাদ দিন এবং পরে পর্যালোচনা করুন + আমরা প্রতিটি রিপোর্ট পড়ি। + নিন + #%1$d + আপনার স্টার থেকে + আলোচিত রিলিজ + সবচেয়ে জনপ্রিয় + এখন ট্রেন্ডিং + আবিষ্কার + সব দেখুন + আরও দেখুন + টোকেন যোগ করুন + পেছনে + বাতিল + মুছুন + সংরক্ষণ + টোকেন যোগ করুন + সনাক্ত করা হয়েছে: %1$s + ফোর্জের ঠিকানা + বিদ্যমান টোকেন রাখতে খালি রাখুন। প্রতিস্থাপন করতে নতুন মান টাইপ করুন। + %1$s এর জন্য টোকেন প্রতিস্থাপন করুন + %1$s এর সাথে সংযুক্ত হবে + একটি Personal Access Token যোগ করতে + এ চাপুন + কোনো টোকেন সংরক্ষিত নেই + প্রতিটি ফোর্জ হোস্টের জন্য Personal Access Token (GitHub, Codeberg, Forgejo) + প্রদর্শনের নাম (ঐচ্ছিক) + Personal Access Token + আপনি অ্যাপের মাধ্যমে GitHub-এ সাইন ইন করেছেন — এটাই প্রস্তাবিত পথ। ব্রাউজার সাইন ইন না চললে (সীমাবদ্ধ নেটওয়ার্ক, ব্রাউজার নেই) অথবা GitHub ছাড়া অন্য ফোর্জের জন্যই কেবল এখানে টোকেন যোগ করুন। + টোকেন পেজ খুলুন + অন্য ফোর্জ + Forgejo, Gitea, সেলফ-হোস্টেড + টোকেন পেস্ট করুন + এর জন্য টোকেন যোগ করুন… + লেবেল সম্পাদনা + অবৈধ · %1$s + আরও + %1$s এ পরিচালনা করুন + টোকেন প্রতিস্থাপন + %1$s অবশিষ্ট + বৈধ · %1$s + পরীক্ষা + %1$s এর জন্য টোকেন সংরক্ষিত হয়েছে + প্রমাণীকরণ টোকেন + পূর্বাবস্থায় ফেরান + %1$s এর জন্য টোকেন পুনরুদ্ধার করা যায়নি + %1$s এর জন্য টোকেন সরানো হয়েছে + যাচাই ব্যর্থ হয়েছে: %1$s + অজানা ত্রুটি + অবৈধ হোস্ট + হোস্ট প্রয়োজন + টোকেন প্রয়োজন + টোকেন খুব ছোট + GitHub Store সম্পর্কে + ফিডব্যাক পাঠান… + ওপেন সোর্স লাইসেন্স + সহায়তা + গোপনীয়তা নীতি + অ্যাপ সেটিংস, থিম, নেটওয়ার্ক, অনুবাদ, উন্নত বিকল্প। + কিছু পরিবর্তন প্রয়োগ করতে পুনরায় চালু করা প্রয়োজন। + পরে + ভাষা + ব্যবহারের তথ্য + থিম + প্রভাবিত: %1$s + এখন পুনরায় চালু করুন + ফিল্টার সরান + সম্পন্ন + ফিল্টার + সব রিসেট করুন + ভাষা + প্ল্যাটফর্ম + সাজান + উৎস + ফলাফল ফিল্টার করুন + অ্যাপ + সংযোগ + ইনস্টল ও আপডেট + চেহারা ও অনুভূতি + গোপনীয়তা ও ডেটা + রিপোজিটরি খোলার সময় README এবং রিলিজ নোট স্বয়ংক্রিয়ভাবে অনুবাদ করুন। আপনার অ্যাপের ভাষা লক্ষ্য হিসেবে ব্যবহৃত হবে। + অ্যাপের ভাষা অনুসরণ করুন (%1$s) + এতে অনুবাদ করুন + রিপোজিটরি স্বয়ংক্রিয় অনুবাদ + বাতিল + ভাষা পরিবর্তন + সনাক্তকৃত উৎস: %1$s + পুনরায় চেষ্টা + মূলটি দেখান + অনুবাদ দেখান + এই পৃষ্ঠাটি অন্য ভাষায় রেন্ডার করুন। + মূলটি দেখানো হচ্ছে + %1$s ভাষায় অনুবাদিত + অনুবাদ করা হচ্ছে… + লক্ষ্য ভাষা + অনুবাদ + %1$s ভাষায় অনুবাদ করুন + Auth key + একটি ফ্রি DeepL key নিন → + deepl.com/pro-api তে সাইন আপ করুন। ফ্রি tier key :fx দিয়ে শেষ হয় এবং স্বয়ংক্রিয়ভাবে ফ্রি এন্ডপয়েন্ট ব্যবহার করে। Pro tier আপনার টেক্সট গোপন রাখে; Free tier মডেল উন্নয়নে জমা দেওয়া টেক্সট ব্যবহার করতে পারে। + Key সংরক্ষণ + DeepL key সংরক্ষিত + API key (ঐচ্ছিক) + ইনস্ট্যান্স URL + পাবলিক Disroot মিরর দিয়ে সরাসরি কাজ করে — এটি ব্যবহার করতে ক্ষেত্রগুলি খালি রাখুন। আরও গোপনীয়তার জন্য, আপনার নিজস্ব সেলফ-হোস্টেড URL পেস্ট করুন। ইনস্ট্যান্সের প্রয়োজন হলেই API key লাগবে। + সেটিংস সংরক্ষণ + LibreTranslate সেটিংস সংরক্ষিত + একটি ফ্রি Azure key নিন → + Azure Translator গোপনীয়তা-সম্মানজনক (ডিফল্টভাবে No-Trace — আপনার টেক্সট কখনো সংরক্ষণ বা প্রশিক্ষণে ব্যবহৃত হয় না)। ফ্রি tier মাসে ২০ লক্ষ অক্ষর কভার করে। নন-গ্লোবাল রিসোর্সের জন্যই কেবল Region প্রয়োজন। + সাবস্ক্রিপশন key + Region (যেমন westeurope, Global-এর জন্য খালি রাখুন) + শংসাপত্র সংরক্ষণ + Microsoft Translator শংসাপত্র সংরক্ষিত + DeepL + LibreTranslate + Microsoft + GitHub Store + অ্যাপে ব্যবহৃত লাইব্রেরি। + ওপেন সোর্স লাইসেন্স + github-store.org এ দেখুন। + গোপনীয়তা নীতি + এই অ্যাপের সোর্স দেখুন। + GitHub-এ সোর্স কোড + GitHub, Codeberg এবং Forgejo রিলিজের জন্য ক্রস-প্ল্যাটফর্ম অ্যাপ স্টোর। + পূর্ববর্তী রিলিজ নোট। + নতুন কী আছে + নিচে ওভাররাইড না করা পর্যন্ত সব ট্রাফিকে প্রযোজ্য। + কাস্টম + নিচে একটি সংযোগ মোড বেছে নিন। বেশিরভাগ মানুষ এটিকে No proxy-তে রাখেন। + অ্যাপ কীভাবে ইন্টারনেটে পৌঁছায় + প্রধান সংযোগ + HTTP/HTTPS + বেশিরভাগ কর্পোরেট প্রক্সি। + কোনো প্রক্সি নয় + SOCKS5 + Tor, SSH টানেল। + সিস্টেম + প্রতিটি স্কোপ ডিফল্টভাবে প্রধান সংযোগ ব্যবহার করে। নিজস্ব সেটিংস রাখতে \'কাস্টম\' বেছে নিন। + প্রতি-স্কোপ ওভাররাইড + সম্পূর্ণ URL পেস্ট করুন + একটি সম্পূর্ণ প্রক্সি URL পেস্ট করুন, আমরা আপনার জন্য ফর্ম পূরণ করব। + এই URL ব্যবহার করুন + সেই URL পড়া যায়নি। + scheme://user:pass@host:port + প্রক্সি URL পেস্ট করুন + GitHub API, অনুসন্ধান, রিপো বিবরণ। + অনুসন্ধান ও মেটাডেটা + APK ও অ্যাসেট ডাউনলোড। + ডাউনলোড + DeepL, Microsoft, LibreTranslate কল। + অনুবাদ + পরীক্ষা + প্রধানটি ব্যবহার করুন + অ্যাক্সেস টোকেন + অ্যাপের তথ্য + চেহারা + সংযোগ + ফিডব্যাক পাঠান + ইনস্টলের পদ্ধতি + ভাষা + গোপনীয়তা + উৎস + স্টোরেজ + পরিচালনা করতে চাপুন + অনুবাদ + আপডেটের আচরণ + নীরব ইনস্টলার ও ইনস্টলার অ্যাট্রিবিউশন Android-এর ফিচার। ডেস্কটপ ইনস্টল OS-এর প্যাকেজ ম্যানেজার দিয়ে হয়। + ইনস্টলের পদ্ধতি শুধু Android-এর জন্য + ভাষা পরিবর্তন করলে অ্যাপ পুনরায় চালু হবে। + প্রকল্প পৃষ্ঠা খুলতে যেকোনো এন্ট্রিতে চাপুন। + GitHub Store এই লাইব্রেরিগুলির উপর দাঁড়িয়ে। + ওপেন সোর্স লাইসেন্স + ব্রাউজিং ইতিহাস + আপনি কোন রিপো খুলেছেন তা ভুলে যান। + এটি কিছুকে আনস্টার বা আনফেভারিট করবে না। + পরিষ্কার করুন + দেখার ইতিহাস পরিষ্কার করবেন? + দেখার ইতিহাস পরিষ্কার করুন + যখন আপনি একটি github.com বা codeberg.org লিংক কপি করবেন, আমরা সেটি খুলতে প্রম্পট করব। + ক্লিপবোর্ডে রিপো লিংক সনাক্ত করুন + যেসব রিপো আপনি ফিড ও সার্চ থেকে মিউট করেছেন। + লুকানো রিপোজিটরি + ফিড ও সার্চে দেখা রিপোগুলি বাদ দিন। + যে রিপো ইতিমধ্যে দেখেছি সেগুলি লুকান + কোন ফিচারগুলি ব্যবহৃত হচ্ছে তা বুঝতে আমাদের সাহায্য করুন। + অ্যাপ সংস্করণ। + ফিচার ব্যবহারের গণনা। + কোনো শনাক্তকারী নেই। + রিপোর নাম নেই। + কোনো টোকেন নেই। + OS এবং প্ল্যাটফর্ম। + বেনামী ব্যবহারের তথ্য শেয়ার করুন + আমরা কী সংগ্রহ করি + ব্যবহারের তথ্য + \'%1$s\' এর সাথে কোনো সেটিংস মেলে না। + সেটিংস অনুসন্ধান করুন + একটি Forgejo বা Gitea হোস্ট যোগ করুন + যোগ করা হোস্ট + GitHub মিরর + অ্যাপ ডিফল্টভাবে GitHub-এ অনুসন্ধান করে। আঞ্চলিক মিরর দিয়ে রুট করুন বা কাস্টম Forgejo / Gitea হোস্ট যোগ করুন। + অ্যাপ কোথায় রিপোজিটরি খোঁজে + ডিফল্ট (github.com) + আপডেট দ্রুত আবার শুরু করতে আমরা ইনস্টলারগুলো রেখে দিই। + পরিষ্কার করুন + ডাউনলোড করা APK + ০ B + ব্যবহার: %1$s + এই স্ক্রিনটি চলমান নতুন ডিজাইনের অংশ। সেটিংসগুলো এখনো কাজ করে — শুধু পুরোনো লেআউট থেকে এখানে সরানো হচ্ছে। + পরবর্তী আপডেটে আসছে diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index f06b137ab..e1b111f02 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -996,4 +996,254 @@ La política de verificación de desarrolladores de Google amenaza la distribución abierta de Android. GitHub Store apoya a la coalición. Saber más Cerrar banner + + + Toca cualquier tarjeta de la izquierda para ver sus detalles aquí. + Elige un repositorio + Listas para instalar + Ajustes avanzados + Acciones rápidas + Ajustes + Estado + Selecciona una app a la izquierda para ver sus detalles y acciones. + Elige una app + Instalada + Última + Abrir repositorio + Elegir variante + Ocultar lista + Ver lista + Revisa o actualiza todo a la vez. + 1 actualización disponible + %1$d actualizaciones disponibles + Atrás + Atrás + Imagen + Ver todo › + + %1$d host + %1$d hosts + + Codeberg, gitea.com y git.disroot.org ya se buscan por defecto — no hace falta añadirlos aquí. Usa esta lista solo para tu propio Forgejo o Gitea autohospedado. + Acerca de + Desarrollador + Estadísticas + Ver perfil + Novedades + Omitir y revisar después + Leemos cada reporte. + Obtener + #%1$d + De tus favoritos + Versiones del momento + Más populares + Tendencias + Descubrir + Ver todo + Ver más + Añadir token + Atrás + Cancelar + Eliminar + Guardar + Añadir token + Detectado: %1$s + Dirección del forge + Déjalo en blanco para conservar el token actual. Escribe un valor nuevo para reemplazarlo. + Reemplazar token de %1$s + Se conectará a %1$s + + %1$d token + %1$d tokens + + Toca + para añadir un token de acceso personal + No hay tokens guardados + Tokens de acceso personal por host de forge (GitHub, Codeberg, Forgejo) + Nombre visible (opcional) + Token de acceso personal + Ya iniciaste sesión en GitHub desde la app — esa es la ruta recomendada. Añade un token aquí solo si el inicio de sesión por navegador no funciona (red restrictiva, sin navegador) o para forges distintos de GitHub. + Abrir página del token + Otro forge + Forgejo, Gitea, autohospedado + Pegar token + Añadir token para… + Editar etiqueta + Inválido · %1$s + Más + Gestionar en %1$s + Reemplazar token + %1$s restantes + Válido · %1$s + Verificar + Token guardado para %1$s + Tokens de autenticación + Deshacer + No se pudo restaurar el token de %1$s + Token de %1$s eliminado + Validación fallida: %1$s + error desconocido + Host inválido + Host requerido + Token requerido + Token demasiado corto + Acerca de GitHub Store + Enviar comentarios… + Licencias de código abierto + Ayuda + Política de privacidad + Ajustes de la app, tema, red, traducción, opciones avanzadas. + Algunos cambios necesitan reiniciar para aplicarse. + Más tarde + idioma + datos de uso + tema + Afectado: %1$s + Reiniciar ahora + Quitar filtro + Listo + Filtros + Restablecer todo + Idioma + Plataforma + Ordenar por + Origen + Filtrar resultados + Aplicación + Conectividad + Instalaciones y actualizaciones + Apariencia + Privacidad y datos + + %1$d app + %1$d apps + + Traduce el README y las notas de versión automáticamente al abrir un repositorio. Usa el idioma de la app como destino. + Sigue el idioma de la app (%1$s) + Traducir a + Traducir repositorios automáticamente + Cancelar + Cambiar idioma + Origen detectado: %1$s + Reintentar + Ver original + Ver traducción + Muestra esta página en otro idioma. + Mostrando el original + Traducido al %1$s + Traduciendo… + Idioma de destino + Traducir + Traducir al %1$s + Clave de autenticación + Obtener una clave gratuita de DeepL → + Regístrate en deepl.com/pro-api. Las claves del plan gratuito terminan en :fx y usan el endpoint gratuito automáticamente. El plan Pro mantiene tu texto privado; el plan gratuito puede usar el texto enviado para mejorar los modelos. + Guardar clave + Clave de DeepL guardada + Clave de API (opcional) + URL de la instancia + Funciona sin configuración mediante el mirror público de Disroot — deja los campos vacíos para usarlo. Para mayor privacidad, pega la URL de tu propia instancia autohospedada. La clave de API solo es necesaria si la instancia la requiere. + Guardar ajustes + Ajustes de LibreTranslate guardados + Obtener una clave gratuita de Azure → + Azure Translator respeta tu privacidad (No-Trace por defecto — tu texto nunca se almacena ni se usa para entrenamiento). El plan gratuito cubre 2 M de caracteres al mes. La región solo es necesaria para recursos no globales. + Clave de suscripción + Región (p. ej. westeurope, déjalo vacío para Global) + Guardar credenciales + Credenciales de Microsoft Translator guardadas + DeepL + LibreTranslate + Microsoft + GitHub Store + Bibliotecas usadas en la app. + Licencias de código abierto + Ver en github-store.org. + Política de privacidad + Ver el código fuente de esta app. + Código fuente en GitHub + Tienda de apps multiplataforma para versiones de GitHub, Codeberg y Forgejo. + Notas de versiones anteriores. + Novedades + Se aplica a todo el tráfico salvo que se sustituya abajo. + Personalizado + Elige un modo de conexión abajo. La mayoría lo deja en Sin proxy. + Cómo accede la app a internet + Conexión principal + HTTP/HTTPS + La mayoría de proxies corporativos. + Sin proxy + SOCKS5 + Tor, túneles SSH. + Sistema + Cada ámbito usa la conexión principal por defecto. Elige \'Personalizado\' para mantener sus propios ajustes. + Sustituciones por ámbito + Pegar URL completa + Pega una URL de proxy completa y rellenaremos el formulario por ti. + Usar esta URL + No se pudo leer esa URL. + esquema://usuario:contraseña@host:puerto + Pegar URL del proxy + API de GitHub, búsqueda, detalles de repo. + Búsqueda y metadatos + Descargas de APK y otros assets. + Descargas + Llamadas a DeepL, Microsoft, LibreTranslate. + Traducción + Probar + Usar principal + Tokens de acceso + Información de la app + Apariencia + Conexión + Enviar comentarios + Método de instalación + Idioma + Privacidad + Fuentes + Almacenamiento + Toca para gestionar + Traducción + Comportamiento de actualizaciones + Los instaladores silenciosos y la atribución de instalador son funciones de Android. Las instalaciones en escritorio pasan por el gestor de paquetes del sistema operativo. + El método de instalación es solo para Android + La app se reinicia al cambiar de idioma. + Toca cualquier entrada para abrir su página del proyecto. + GitHub Store se apoya en estas bibliotecas. + Licencias de código abierto + Historial de navegación + Olvidar qué repos ya has abierto. + Esto no quitará favoritos ni estrellas a nada. + Borrar + ¿Borrar el historial de vistos? + Borrar historial de vistos + Cuando copies un enlace de github.com o codeberg.org, te preguntaremos si quieres abrirlo. + Detectar enlaces de repo en el portapapeles + Repos que has silenciado en los feeds y la búsqueda. + Repositorios ocultos + Omite los repos vistos en feeds y búsqueda. + Ocultar repos que ya he visto + Ayúdanos a entender qué funciones se usan. + Versión de la app. + Recuentos de uso de funciones. + Sin identificadores. + Sin nombres de repos. + Sin tokens. + Sistema operativo y plataforma. + Compartir datos de uso anónimos + Qué recopilamos + Datos de uso + Ningún ajuste coincide con \'%1$s\'. + Buscar ajustes + Añadir un host de Forgejo o Gitea + Hosts añadidos + Mirror de GitHub + La app busca en GitHub por defecto. Enruta por un mirror regional o añade hosts personalizados de Forgejo / Gitea. + Dónde busca la app los repositorios + Predeterminado (github.com) + Conservamos los instaladores para que las actualizaciones se reanuden rápido. + Borrar + APK descargados + 0 B + En uso: %1$s + Esta pantalla forma parte del rediseño en curso. Los ajustes siguen funcionando — solo los estamos moviendo aquí desde el diseño anterior. + Llegará en una próxima versión diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index f6cb3b716..e1831306f 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -997,4 +997,253 @@ La politique de vérification des développeurs de Google menace la distribution ouverte d'Android. GitHub Store soutient la coalition. En savoir plus Fermer la bannière + + Touchez une carte à gauche pour voir ses détails ici. + Choisissez un dépôt + Prêt à installer + Paramètres avancés + Actions rapides + Paramètres + État + Sélectionnez une app à gauche pour voir ses détails et actions. + Choisissez une app + Installée + Dernière + Ouvrir le dépôt + Choisir la variante + Masquer la liste + Voir la liste + Examinez ou mettez tout à jour d\'un coup. + 1 mise à jour disponible + %1$d mises à jour disponibles + Retour + Retour + Image + Tout voir › + + %1$d hôte + %1$d hôtes + + Codeberg, gitea.com et git.disroot.org sont déjà interrogés par défaut — inutile de les ajouter ici. Cette liste sert uniquement à vos propres instances Forgejo ou Gitea auto-hébergées. + À propos + Développeur + Statistiques + Voir le profil + Nouveautés + Ignorer et examiner plus tard + Nous lisons chaque retour. + Obtenir + n°%1$d + Depuis vos favoris + Versions chaudes + Les plus populaires + Tendances du moment + Découvrir + Tout voir + Voir plus + Ajouter un jeton + Retour + Annuler + Supprimer + Enregistrer + Ajouter un jeton + Détecté : %1$s + Adresse de la forge + Laissez vide pour conserver le jeton existant. Saisissez une nouvelle valeur pour le remplacer. + Remplacer le jeton pour %1$s + Se connectera à %1$s + + %1$d jeton + %1$d jetons + + Touchez + pour ajouter un Personal Access Token + Aucun jeton enregistré + Personal access tokens par hôte de forge (GitHub, Codeberg, Forgejo) + Nom d\'affichage (facultatif) + Personal access token + Vous êtes connecté à GitHub via l\'application — c\'est la méthode recommandée. N\'ajoutez un jeton ici que si la connexion par navigateur ne fonctionne pas (réseau restrictif, pas de navigateur) ou pour des forges autres que GitHub. + Ouvrir la page des jetons + Autre forge + Forgejo, Gitea, auto-hébergé + Coller le jeton + Ajouter un jeton pour… + Modifier l\'étiquette + Invalide · %1$s + Plus + Gérer sur %1$s + Remplacer le jeton + %1$s restants + Valide · %1$s + Vérifier + Jeton enregistré pour %1$s + Jetons d\'authentification + Annuler + Impossible de restaurer le jeton pour %1$s + Jeton pour %1$s supprimé + Échec de la validation : %1$s + erreur inconnue + Hôte invalide + Hôte requis + Jeton requis + Jeton trop court + À propos de GitHub Store + Envoyer un commentaire… + Licences open source + Aide + Politique de confidentialité + Paramètres, thème, réseau, traduction, options avancées. + Certains changements nécessitent un redémarrage pour s\'appliquer. + Plus tard + langue + données d\'usage + thème + Concerné : %1$s + Redémarrer + Retirer le filtre + Terminé + Filtres + Tout réinitialiser + Langage + Plateforme + Trier par + Source + Filtrer les résultats + Application + Connectivité + Installations & mises à jour + Apparence + Confidentialité & données + + %1$d app + %1$d apps + + Traduit automatiquement le README et les notes de version à l\'ouverture d\'un dépôt. Utilise la langue de l\'app comme cible. + Selon la langue de l\'app (%1$s) + Traduire en + Traduire les dépôts automatiquement + Annuler + Changer de langue + Source détectée : %1$s + Réessayer + Afficher l\'original + Afficher la traduction + Affichez cette page dans une autre langue. + Affichage de l\'original + Traduit en %1$s + Traduction… + Langue cible + Traduire + Traduire en %1$s + Clé d\'authentification + Obtenir une clé DeepL gratuite → + Inscrivez-vous sur deepl.com/pro-api. Les clés du niveau gratuit se terminent par :fx et utilisent automatiquement l\'endpoint gratuit. Le niveau Pro préserve la confidentialité de votre texte ; le niveau gratuit peut utiliser les textes soumis pour améliorer les modèles. + Enregistrer la clé + Clé DeepL enregistrée + Clé API (facultative) + URL de l\'instance + Fonctionne d\'emblée via le miroir public Disroot — laissez les champs vides pour l\'utiliser. Pour plus de confidentialité, collez l\'URL de votre instance auto-hébergée. Une clé API n\'est nécessaire que si l\'instance l\'exige. + Enregistrer + Paramètres LibreTranslate enregistrés + Obtenir une clé Azure gratuite → + Azure Translator respecte la vie privée (No-Trace par défaut — votre texte n\'est jamais stocké ni utilisé pour l\'entraînement). Le niveau gratuit couvre 2 millions de caractères par mois. La région n\'est nécessaire que pour les ressources non globales. + Clé d\'abonnement + Région (ex. westeurope, laissez vide pour Global) + Enregistrer les identifiants + Identifiants Microsoft Translator enregistrés + DeepL + LibreTranslate + Microsoft + GitHub Store + Bibliothèques utilisées par l\'application. + Licences open source + Consulter sur github-store.org. + Politique de confidentialité + Consulter le code source de l\'application. + Code source sur GitHub + Boutique d\'applications multi-plateforme pour les versions GitHub, Codeberg et Forgejo. + Notes des versions précédentes. + Nouveautés + S\'applique à tout le trafic sauf si remplacé ci-dessous. + Personnalisé + Choisissez un mode de connexion ci-dessous. La plupart des utilisateurs gardent « Aucun proxy ». + Comment l\'application accède à internet + Connexion principale + HTTP/HTTPS + La plupart des proxys d\'entreprise. + Aucun proxy + SOCKS5 + Tor, tunnels SSH. + Système + Chaque portée utilise la connexion principale par défaut. Choisissez « Personnalisé » pour conserver des paramètres dédiés. + Remplacements par portée + Coller l\'URL complète + Collez une URL de proxy complète et nous remplirons le formulaire pour vous. + Utiliser cette URL + Impossible de lire cette URL. + scheme://user:pass@host:port + Coller l\'URL du proxy + API GitHub, recherche, détails du dépôt. + Recherche & métadonnées + Téléchargements d\'APK et d\'assets. + Téléchargements + Appels DeepL, Microsoft, LibreTranslate. + Traduction + Tester + Utiliser la principale + Jetons d\'accès + Infos de l\'app + Apparence + Connexion + Envoyer un commentaire + Méthode d\'installation + Langue + Confidentialité + Sources + Stockage + Toucher pour gérer + Traduction + Comportement des mises à jour + Les installateurs silencieux et l\'attribution d\'installeur sont des fonctionnalités Android. Sur ordinateur, les installations passent par le gestionnaire de paquets du système. + La méthode d\'installation est réservée à Android + L\'application redémarre lors du changement de langue. + Touchez une entrée pour ouvrir la page du projet. + GitHub Store s\'appuie sur ces bibliothèques. + Licences open source + Historique de navigation + Oublier quels dépôts vous avez déjà ouverts. + Cela ne retire pas vos étoiles ni vos favoris. + Effacer + Effacer l\'historique des consultations ? + Effacer l\'historique des consultations + Lorsque vous copiez un lien github.com ou codeberg.org, nous vous proposons de l\'ouvrir. + Détecter les liens de dépôts dans le presse-papiers + Dépôts que vous avez masqués des flux et de la recherche. + Dépôts masqués + Ignorer les dépôts déjà vus dans les flux et la recherche. + Masquer les dépôts déjà consultés + Aidez-nous à comprendre quelles fonctionnalités sont utilisées. + Version de l\'application. + Nombre d\'utilisations des fonctionnalités. + Aucun identifiant. + Aucun nom de dépôt. + Aucun jeton. + Système d\'exploitation et plateforme. + Partager des données d\'usage anonymes + Ce que nous collectons + Données d\'usage + Aucun paramètre ne correspond à « %1$s ». + Rechercher dans les paramètres + Ajouter un hôte Forgejo ou Gitea + Hôtes ajoutés + Miroir GitHub + L\'application interroge GitHub par défaut. Passez par un miroir régional ou ajoutez des hôtes Forgejo / Gitea personnalisés. + Où l\'application cherche les dépôts + Par défaut (github.com) + Nous conservons les installateurs pour que les mises à jour reprennent rapidement. + Vider + APK téléchargés + 0 o + Utilisation : %1$s + Cet écran fait partie de la refonte en cours. Les paramètres fonctionnent toujours — ils sont simplement en cours de migration depuis l\'ancienne disposition. + Bientôt disponible diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index cab664a3b..297cae707 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1028,4 +1028,262 @@ Google की डेवलपर-सत्यापन नीति खुले Android वितरण को खतरे में डालती है। GitHub Store गठबंधन के साथ है। और जानें बैनर बंद करें + + + + यहाँ इसके विवरण देखने के लिए बाईं ओर किसी कार्ड पर टैप करें। + एक रिपॉजिटरी चुनें + इंस्टॉल के लिए तैयार + उन्नत सेटिंग्स + त्वरित क्रियाएँ + सेटिंग्स + स्थिति + इसके विवरण और क्रियाएँ देखने के लिए बाईं ओर किसी ऐप का चयन करें। + एक ऐप चुनें + स्थापित + नवीनतम + रिपॉजिटरी खोलें + वेरिएंट चुनें + सूची छिपाएँ + सूची देखें + एक साथ सब कुछ देखें या अपडेट करें। + 1 अपडेट उपलब्ध + %1$d अपडेट उपलब्ध + वापस + वापस + छवि + सभी देखें › + Codeberg, gitea.com और git.disroot.org पहले से डिफ़ॉल्ट रूप से खोजे जाते हैं — आपको उन्हें यहाँ जोड़ने की ज़रूरत नहीं है। इस सूची का उपयोग केवल अपने सेल्फ-होस्टेड Forgejo या Gitea के लिए करें। + परिचय + डेवलपर + आँकड़े + प्रोफ़ाइल देखें + नया क्या है + छोड़ें और बाद में समीक्षा करें + हम हर रिपोर्ट पढ़ते हैं। + पाएँ + #%1$d + आपके स्टार से + गर्म रिलीज़ + सर्वाधिक लोकप्रिय + अभी ट्रेंडिंग + खोजें + सभी देखें + और देखें + टोकन जोड़ें + वापस + रद्द करें + हटाएँ + सहेजें + टोकन जोड़ें + पहचाना गया: %1$s + फोर्ज पता + मौजूदा टोकन रखने के लिए खाली छोड़ें। बदलने के लिए नया मान टाइप करें। + %1$s के लिए टोकन बदलें + %1$s से कनेक्ट करेगा + पर्सनल एक्सेस टोकन जोड़ने के लिए + पर टैप करें + कोई टोकन संग्रहीत नहीं + प्रति फोर्ज होस्ट के लिए पर्सनल एक्सेस टोकन (GitHub, Codeberg, Forgejo) + डिस्प्ले नाम (वैकल्पिक) + पर्सनल एक्सेस टोकन + आप ऐप के ज़रिए GitHub में साइन इन हैं — यही अनुशंसित रास्ता है। यहाँ टोकन केवल तभी जोड़ें जब ब्राउज़र साइन-इन काम न करे (प्रतिबंधित नेटवर्क, ब्राउज़र नहीं) या GitHub के अलावा किसी अन्य फोर्ज के लिए। + टोकन पेज खोलें + अन्य फोर्ज + Forgejo, Gitea, सेल्फ-होस्टेड + टोकन पेस्ट करें + इसके लिए टोकन जोड़ें… + लेबल संपादित करें + अमान्य · %1$s + अधिक + %1$s पर प्रबंधित करें + टोकन बदलें + %1$s शेष + मान्य · %1$s + जाँचें + %1$s के लिए टोकन सहेजा गया + प्रमाणीकरण टोकन + पूर्ववत करें + %1$s के लिए टोकन पुनर्स्थापित नहीं हो सका + %1$s के लिए टोकन हटाया गया + सत्यापन विफल: %1$s + अज्ञात त्रुटि + अमान्य होस्ट + होस्ट आवश्यक + टोकन आवश्यक + टोकन बहुत छोटा + GitHub Store के बारे में + फ़ीडबैक भेजें… + ओपन सोर्स लाइसेंस + सहायता + गोपनीयता नीति + ऐप सेटिंग्स, थीम, नेटवर्क, अनुवाद, उन्नत विकल्प। + कुछ बदलाव लागू करने के लिए पुनरारंभ की आवश्यकता है। + बाद में + भाषा + उपयोग डेटा + थीम + प्रभावित: %1$s + अभी पुनरारंभ करें + फ़िल्टर हटाएँ + हो गया + फ़िल्टर + सभी रीसेट करें + भाषा + प्लेटफ़ॉर्म + क्रमबद्ध करें + स्रोत + परिणाम फ़िल्टर करें + ऐप + कनेक्टिविटी + इंस्टॉल और अपडेट + रूप और अनुभव + गोपनीयता और डेटा + रिपॉजिटरी खोलने पर README और रिलीज़ नोट्स स्वचालित रूप से अनुवाद करें। आपकी ऐप भाषा को लक्ष्य के रूप में उपयोग करता है। + ऐप भाषा का अनुसरण करें (%1$s) + अनुवाद करें + रिपॉजिटरी स्वतः-अनुवाद करें + रद्द करें + भाषा बदलें + पहचाना गया स्रोत: %1$s + पुनः प्रयास करें + मूल दिखाएँ + अनुवाद दिखाएँ + इस पृष्ठ को किसी अन्य भाषा में देखें। + मूल दिखाया जा रहा है + %1$s में अनुवादित + अनुवाद हो रहा है… + लक्ष्य भाषा + अनुवाद करें + %1$s में अनुवाद करें + Auth key + मुफ़्त DeepL key पाएँ → + deepl.com/pro-api पर साइन अप करें। मुफ़्त स्तर की keys :fx से समाप्त होती हैं और स्वचालित रूप से मुफ़्त एंडपॉइंट का उपयोग करती हैं। Pro स्तर आपके टेक्स्ट को निजी रखता है; मुफ़्त स्तर भेजे गए टेक्स्ट का उपयोग मॉडल सुधार के लिए कर सकता है। + key सहेजें + DeepL key सहेजी गई + API key (वैकल्पिक) + Instance URL + सार्वजनिक Disroot मिरर के माध्यम से तुरंत काम करता है — इसे उपयोग करने के लिए फ़ील्ड खाली छोड़ें। अधिक गोपनीयता के लिए, अपना सेल्फ-होस्टेड URL पेस्ट करें। API key केवल तभी आवश्यक है जब इंस्टेंस को इसकी ज़रूरत हो। + सेटिंग्स सहेजें + LibreTranslate सेटिंग्स सहेजी गईं + मुफ़्त Azure key पाएँ → + Azure Translator गोपनीयता का सम्मान करता है (डिफ़ॉल्ट रूप से No-Trace — आपका टेक्स्ट कभी संग्रहीत नहीं होता और न ही प्रशिक्षण के लिए उपयोग होता है)। मुफ़्त स्तर 2M वर्ण/माह कवर करता है। क्षेत्र केवल गैर-वैश्विक संसाधनों के लिए आवश्यक है। + सब्सक्रिप्शन key + क्षेत्र (जैसे westeurope, Global के लिए खाली छोड़ें) + क्रेडेंशियल्स सहेजें + Microsoft Translator क्रेडेंशियल्स सहेजे गए + DeepL + LibreTranslate + Microsoft + GitHub Store + ऐप में उपयोग की गई लाइब्रेरियाँ। + ओपन सोर्स लाइसेंस + github-store.org पर देखें। + गोपनीयता नीति + इस ऐप का सोर्स देखें। + GitHub पर सोर्स कोड + GitHub, Codeberg और Forgejo रिलीज़ के लिए क्रॉस-प्लेटफ़ॉर्म ऐप स्टोर। + पिछले रिलीज़ नोट्स। + नया क्या है + जब तक नीचे ओवरराइड न हो, सभी ट्रैफ़िक पर लागू होता है। + कस्टम + नीचे एक कनेक्शन मोड चुनें। अधिकांश लोग इसे No proxy पर छोड़ देते हैं। + ऐप इंटरनेट तक कैसे पहुँचता है + मुख्य कनेक्शन + HTTP/HTTPS + अधिकांश कॉर्पोरेट प्रॉक्सी। + कोई प्रॉक्सी नहीं + SOCKS5 + Tor, SSH टनल। + सिस्टम + प्रत्येक स्कोप डिफ़ॉल्ट रूप से मुख्य कनेक्शन का उपयोग करता है। अपनी सेटिंग्स रखने के लिए \'कस्टम\' चुनें। + प्रति-स्कोप ओवरराइड + पूरा URL पेस्ट करें + एक पूरा प्रॉक्सी URL पेस्ट करें और हम आपके लिए फ़ॉर्म भर देंगे। + इस URL का उपयोग करें + वह URL पढ़ा नहीं जा सका। + scheme://user:pass@host:port + प्रॉक्सी URL पेस्ट करें + GitHub API, खोज, रिपो विवरण। + खोज और मेटाडेटा + APK और एसेट डाउनलोड। + डाउनलोड + DeepL, Microsoft, LibreTranslate कॉल। + अनुवाद + परीक्षण + मुख्य का उपयोग करें + एक्सेस टोकन + ऐप जानकारी + रूप + कनेक्शन + फ़ीडबैक भेजें + इंस्टॉल विधि + भाषा + गोपनीयता + स्रोत + संग्रहण + प्रबंधन के लिए टैप करें + अनुवाद + अपडेट व्यवहार + साइलेंट इंस्टॉलर और इंस्टॉलर एट्रिब्यूशन Android-केवल सुविधाएँ हैं। डेस्कटॉप इंस्टॉल OS पैकेज मैनेजर के माध्यम से होते हैं। + इंस्टॉल विधि Android-केवल है + भाषा बदलने पर ऐप पुनरारंभ होता है। + किसी भी प्रविष्टि को उसके प्रोजेक्ट पृष्ठ पर खोलने के लिए टैप करें। + GitHub Store इन लाइब्रेरियों पर खड़ा है। + ओपन सोर्स लाइसेंस + ब्राउज़िंग इतिहास + भूल जाएँ कि आपने कौन सी रिपो पहले से खोली हैं। + यह किसी भी चीज़ को अनस्टार या अनफ़ेवरेट नहीं करेगा। + साफ़ करें + देखा गया इतिहास साफ़ करें? + देखा गया इतिहास साफ़ करें + जब आप github.com या codeberg.org लिंक कॉपी करते हैं, तो हम इसे खोलने का संकेत देंगे। + क्लिपबोर्ड में रिपो लिंक पहचानें + वे रिपो जिन्हें आपने फ़ीड और खोज से म्यूट किया है। + छिपी हुई रिपॉजिटरी + फ़ीड और खोज में देखी हुई रिपो छोड़ें। + मेरे द्वारा पहले से देखी गई रिपो छिपाएँ + हमें यह समझने में मदद करें कि कौन सी सुविधाओं का उपयोग किया जाता है। + ऐप संस्करण। + सुविधा उपयोग गणना। + कोई पहचानकर्ता नहीं। + कोई रिपो नाम नहीं। + कोई टोकन नहीं। + OS और प्लेटफ़ॉर्म। + अनाम उपयोग डेटा साझा करें + हम क्या एकत्र करते हैं + उपयोग डेटा + कोई सेटिंग \'%1$s\' से मेल नहीं खाती। + सेटिंग्स खोजें + Forgejo या Gitea होस्ट जोड़ें + जोड़े गए होस्ट + GitHub मिरर + ऐप डिफ़ॉल्ट रूप से GitHub खोजता है। क्षेत्रीय मिरर के माध्यम से रूट करें या कस्टम Forgejo / Gitea होस्ट जोड़ें। + ऐप रिपॉजिटरी कहाँ देखता है + डिफ़ॉल्ट (github.com) + हम इंस्टॉलर रखते हैं ताकि अपडेट तेज़ी से फिर शुरू हो सकें। + साफ़ करें + डाउनलोड किए गए APK + 0 B + उपयोग में: %1$s + यह स्क्रीन प्रगति में पुनर्डिज़ाइन का हिस्सा है। सेटिंग्स अभी भी काम करती हैं — उन्हें पुराने लेआउट से यहाँ स्थानांतरित किया जा रहा है। + अगले अपडेट में आ रहा है + + + %1$d होस्ट + %1$d होस्ट + + + %1$d रिपो + %1$d रिपो + + + %1$d टोकन + %1$d टोकन + + + %1$d ऐप + %1$d ऐप + diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index de489724d..71a5ad282 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1030,4 +1030,254 @@ La policy di verifica degli sviluppatori di Google minaccia la distribuzione aperta di Android. GitHub Store sostiene la coalizione. Scopri di più Chiudi banner + + + Tocca una scheda a sinistra per vederne i dettagli qui. + Scegli un repository + Pronte da installare + Impostazioni avanzate + Azioni rapide + Impostazioni + Stato + Seleziona un'app a sinistra per vedere dettagli e azioni. + Scegli un'app + Installata + Ultima + Apri repository + Scegli variante + Nascondi elenco + Vedi elenco + Esamina o aggiorna tutto in una volta. + 1 aggiornamento disponibile + %1$d aggiornamenti disponibili + Indietro + Indietro + Immagine + Vedi tutto › + + %1$d host + %1$d host + + Codeberg, gitea.com e git.disroot.org vengono già cercati per impostazione predefinita — non c'è bisogno di aggiungerli qui. Usa questo elenco solo per i tuoi Forgejo o Gitea self-hosted. + Informazioni + Sviluppatore + Statistiche + Vedi profilo + Cosa c'è di nuovo + Salta e rivedi più tardi + Leggiamo ogni segnalazione. + Ottieni + #%1$d + Dai tuoi preferiti + Release in evidenza + I più popolari + Di tendenza ora + Scopri + Vedi tutto + Vedi altri + Aggiungi token + Indietro + Annulla + Elimina + Salva + Aggiungi token + Rilevato: %1$s + Indirizzo forge + Lascia vuoto per mantenere il token esistente. Inserisci un nuovo valore per sostituirlo. + Sostituisci token per %1$s + Si connetterà a %1$s + + %1$d token + %1$d token + + Tocca + per aggiungere un personal access token + Nessun token salvato + Personal access token per ciascun host forge (GitHub, Codeberg, Forgejo) + Nome visualizzato (facoltativo) + Personal access token + Hai effettuato l'accesso a GitHub dall'app — è il metodo consigliato. Aggiungi un token qui solo se l'accesso dal browser non funziona (rete restrittiva, niente browser) o per forge diverse da GitHub. + Apri pagina token + Altra forge + Forgejo, Gitea, self-hosted + Incolla token + Aggiungi token per… + Modifica etichetta + Non valido · %1$s + Altro + Gestisci su %1$s + Sostituisci token + %1$s rimanenti + Valido · %1$s + Verifica + Token salvato per %1$s + Token di autenticazione + Annulla + Impossibile ripristinare il token per %1$s + Token per %1$s rimosso + Validazione fallita: %1$s + errore sconosciuto + Host non valido + Host obbligatorio + Token obbligatorio + Token troppo corto + Informazioni su GitHub Store + Invia feedback… + Licenze open source + Aiuto + Informativa sulla privacy + Impostazioni app, tema, rete, traduzione, opzioni avanzate. + Alcune modifiche richiedono un riavvio per essere applicate. + Più tardi + lingua + dati di utilizzo + tema + Interessati: %1$s + Riavvia ora + Rimuovi filtro + Fatto + Filtri + Reimposta tutto + Lingua + Piattaforma + Ordina per + Origine + Filtra risultati + App + Connettività + Installazioni e aggiornamenti + Aspetto + Privacy e dati + + %1$d app + %1$d app + + Traduci automaticamente README e note di rilascio quando apri un repository. Usa la lingua dell'app come destinazione. + Segui la lingua dell'app (%1$s) + Traduci in + Traduci repository automaticamente + Annulla + Cambia lingua + Origine rilevata: %1$s + Riprova + Mostra originale + Mostra traduzione + Visualizza questa pagina in un'altra lingua. + Versione originale + Tradotto in %1$s + Traduzione in corso… + Lingua di destinazione + Traduci + Traduci in %1$s + Auth key + Ottieni una key DeepL gratuita → + Registrati su deepl.com/pro-api. Le key del piano gratuito finiscono con :fx e usano automaticamente l'endpoint free. Il piano Pro mantiene privato il tuo testo; il piano Free potrebbe usare il testo inviato per migliorare i modelli. + Salva key + Key DeepL salvata + API key (facoltativa) + URL dell'istanza + Funziona subito tramite il mirror pubblico Disroot — lascia i campi vuoti per usarlo. Per maggiore privacy, incolla l'URL della tua istanza self-hosted. La API key serve solo se l'istanza lo richiede. + Salva impostazioni + Impostazioni LibreTranslate salvate + Ottieni una key Azure gratuita → + Azure Translator rispetta la privacy (No-Trace per impostazione predefinita — il testo non viene mai memorizzato né usato per l'addestramento). Il piano gratuito copre 2M di caratteri al mese. La regione serve solo per risorse non globali. + Subscription key + Regione (es. westeurope, lascia vuoto per Global) + Salva credenziali + Credenziali Microsoft Translator salvate + DeepL + LibreTranslate + Microsoft + GitHub Store + Librerie usate nell'app. + Licenze open source + Vedi su github-store.org. + Informativa sulla privacy + Vedi il codice sorgente dell'app. + Codice sorgente su GitHub + App store multipiattaforma per release di GitHub, Codeberg e Forgejo. + Note delle release precedenti. + Cosa c'è di nuovo + Si applica a tutto il traffico salvo override qui sotto. + Personalizzato + Scegli una modalità di connessione qui sotto. Nella maggior parte dei casi lascia su Nessun proxy. + Come l'app raggiunge internet + Connessione principale + HTTP/HTTPS + La maggior parte dei proxy aziendali. + Nessun proxy + SOCKS5 + Tor, tunnel SSH. + Sistema + Ogni ambito usa la connessione principale per impostazione predefinita. Scegli \'Personalizzato\' per mantenerne uno proprio. + Override per ambito + Incolla URL completo + Incolla un URL proxy completo e compileremo noi il modulo. + Usa questo URL + Impossibile leggere quell\'URL. + scheme://user:pass@host:port + Incolla URL del proxy + API di GitHub, ricerca, dettagli repo. + Ricerca e metadati + Download di APK e asset. + Download + Chiamate a DeepL, Microsoft, LibreTranslate. + Traduzione + Testa + Usa principale + Token di accesso + Informazioni app + Aspetto + Connessione + Invia feedback + Metodo di installazione + Lingua + Privacy + Sorgenti + Archiviazione + Tocca per gestire + Traduzione + Comportamento aggiornamenti + Gli installer silenziosi e l'attribuzione installer sono funzioni Android. Su desktop l'installazione passa per il package manager del sistema. + Il metodo di installazione è solo Android + L'app si riavvia quando cambi lingua. + Tocca una voce per aprirne la pagina del progetto. + GitHub Store si basa su queste librerie. + Licenze open source + Cronologia di navigazione + Dimentica quali repo hai già aperto. + Non rimuoverà stelle né preferiti. + Cancella + Cancellare la cronologia visualizzata? + Cancella cronologia visualizzata + Quando copi un link github.com o codeberg.org, ti chiederemo di aprirlo. + Rileva link a repo negli appunti + Repo che hai silenziato da feed e ricerca. + Repository nascosti + Salta i repo già visti in feed e ricerca. + Nascondi repo già visualizzati + Aiutaci a capire quali funzionalità vengono usate. + Versione dell'app. + Conteggi di utilizzo delle funzionalità. + Nessun identificatore. + Nessun nome di repo. + Nessun token. + Sistema operativo e piattaforma. + Condividi dati di utilizzo anonimi + Cosa raccogliamo + Dati di utilizzo + Nessuna impostazione corrisponde a \'%1$s\'. + Cerca impostazioni + Aggiungi un host Forgejo o Gitea + Host aggiunti + Mirror GitHub + L'app cerca su GitHub per impostazione predefinita. Instrada tramite un mirror regionale o aggiungi host Forgejo / Gitea personalizzati. + Dove l'app cerca i repository + Predefinito (github.com) + Conserviamo gli installer per riprendere gli aggiornamenti rapidamente. + Cancella + APK scaricati + 0 B + In uso: %1$s + Questa schermata fa parte del redesign in corso. Le impostazioni funzionano ancora — vengono solo spostate qui dal vecchio layout. + In arrivo diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 37fbbc39b..80fedb0f7 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -991,4 +991,250 @@ Google の開発者検証ポリシーが Android のオープンな配布を脅かしています。GitHub Store は連合を支持します。 詳しく見る バナーを閉じる + + 左側のカードをタップすると詳細がここに表示されます。 + リポジトリを選択 + インストール待ち + 詳細設定 + クイック操作 + 設定 + ステータス + 左側のアプリを選択すると、詳細と操作が表示されます。 + アプリを選択 + インストール済み + 最新 + リポジトリを開く + バリアントを選択 + リストを隠す + リストを見る + すべてをまとめて確認・更新できます。 + 1 件の更新があります + %1$d 件の更新があります + 戻る + 戻る + 画像 + すべて見る › + + %1$d 件のホスト + + Codeberg、gitea.com、git.disroot.org はデフォルトで検索されるため、ここに追加する必要はありません。このリストは自分でホストしている Forgejo や Gitea 専用です。 + 概要 + 開発者 + 統計 + プロフィールを見る + 新着情報 + スキップして後で確認 + すべてのご報告に目を通しています。 + 取得 + 第%1$d位 + スター付きから + 注目のリリース + 人気上位 + トレンド + 見つける + すべて見る + もっと見る + トークンを追加 + 戻る + キャンセル + 削除 + 保存 + トークンを追加 + 検出: %1$s + フォージのアドレス + 既存のトークンを維持するには空欄のままにしてください。新しい値を入力すると置き換えられます。 + %1$s のトークンを置き換え + %1$s に接続します + + %1$d 件のトークン + + + をタップして個人アクセストークンを追加してください + 保存されたトークンはありません + フォージホスト(GitHub、Codeberg、Forgejo)ごとの個人アクセストークン + 表示名(任意) + 個人アクセストークン + アプリ経由で GitHub にサインイン済みです — こちらが推奨される方法です。ブラウザでのサインインがうまくいかない場合(制限付きネットワーク、ブラウザなし)や、GitHub 以外のフォージでのみ、ここにトークンを追加してください。 + トークンページを開く + その他のフォージ + Forgejo、Gitea、セルフホスト + トークンを貼り付け + トークンを追加するホスト… + ラベルを編集 + 無効 ・ %1$s + その他 + %1$s で管理 + トークンを置き換え + 残り %1$s + 有効 ・ %1$s + 確認 + %1$s のトークンを保存しました + 認証トークン + 元に戻す + %1$s のトークンを復元できませんでした + %1$s のトークンを削除しました + 検証に失敗しました: %1$s + 不明なエラー + ホストが無効です + ホストは必須です + トークンは必須です + トークンが短すぎます + GitHub Store について + フィードバックを送る… + オープンソースライセンス + ヘルプ + プライバシーポリシー + アプリ設定、テーマ、ネットワーク、翻訳、詳細オプション。 + 一部の変更を反映するには再起動が必要です。 + 後で + 言語 + 使用状況データ + テーマ + 対象: %1$s + 今すぐ再起動 + フィルターを解除 + 完了 + フィルター + すべてリセット + 言語 + プラットフォーム + 並び替え + ソース + 結果を絞り込む + アプリ + 接続 + インストールと更新 + 外観 + プライバシーとデータ + + %1$d 件のアプリ + + リポジトリを開いたときに README とリリースノートを自動翻訳します。アプリの言語を翻訳先として使用します。 + アプリの言語に従う(%1$s) + 翻訳先 + リポジトリを自動翻訳 + キャンセル + 言語を変更 + 検出された言語: %1$s + 再試行 + 原文を表示 + 訳文を表示 + このページを別の言語で表示します。 + 原文を表示中 + %1$s に翻訳しました + 翻訳中… + 翻訳先言語 + 翻訳 + %1$s に翻訳 + 認証キー + 無料の DeepL キーを取得 → + deepl.com/pro-api からサインアップしてください。無料プランのキーは :fx で終わり、自動的に無料エンドポイントを使用します。Pro プランは入力テキストを非公開に保ちますが、Free プランでは送信されたテキストがモデル改善に使われる場合があります。 + キーを保存 + DeepL キーを保存しました + API キー(任意) + インスタンス URL + 公開されている Disroot ミラー経由で初期状態で動作します — 各欄を空のままにすれば使用されます。よりプライバシーを重視する場合は、ご自身のセルフホスト URL を貼り付けてください。API キーはインスタンスが要求する場合にのみ必要です。 + 設定を保存 + LibreTranslate の設定を保存しました + 無料の Azure キーを取得 → + Azure Translator はプライバシーに配慮しています(デフォルトで No-Trace — 入力テキストは保存も学習にも使われません)。無料プランは月間 200 万文字までカバーします。Region は非グローバルリソースの場合のみ必要です。 + サブスクリプションキー + Region(例: westeurope。グローバルの場合は空欄) + 認証情報を保存 + Microsoft Translator の認証情報を保存しました + DeepL + LibreTranslate + Microsoft + GitHub Store + アプリで使用しているライブラリ。 + オープンソースライセンス + github-store.org で表示。 + プライバシーポリシー + このアプリのソースコードを見る。 + GitHub のソースコード + GitHub、Codeberg、Forgejo リリース向けのクロスプラットフォーム アプリストア。 + 過去のリリースノート。 + 新着情報 + 下記で個別に上書きしない限り、すべての通信に適用されます。 + カスタム + 以下から接続モードを選択してください。多くの方は「プロキシなし」のままで問題ありません。 + アプリのインターネット接続方法 + メイン接続 + HTTP/HTTPS + 大半の社内プロキシ。 + プロキシなし + SOCKS5 + Tor、SSH トンネル。 + システム + 各スコープはデフォルトでメイン接続を使用します。「カスタム」を選ぶと、個別の設定を保持できます。 + スコープ別の上書き + 完全な URL を貼り付け + 完全なプロキシ URL を貼り付ければ、フォームを自動で入力します。 + この URL を使う + この URL を読み取れませんでした。 + scheme://user:pass@host:port + プロキシ URL を貼り付け + GitHub API、検索、リポジトリ詳細。 + 検索とメタデータ + APK とアセットのダウンロード。 + ダウンロード + DeepL、Microsoft、LibreTranslate の通信。 + 翻訳 + テスト + メインを使う + アクセストークン + アプリ情報 + 外観 + 接続 + フィードバックを送る + インストール方法 + 言語 + プライバシー + ソース + ストレージ + タップして管理 + 翻訳 + 更新の挙動 + サイレントインストールとインストーラー帰属は Android 限定の機能です。デスクトップ版のインストールは OS のパッケージマネージャー経由で行われます。 + インストール方法は Android 専用です + 言語を切り替えるとアプリが再起動します。 + いずれかの項目をタップするとプロジェクトページが開きます。 + GitHub Store はこれらのライブラリに支えられています。 + オープンソースライセンス + 閲覧履歴 + 開いたことのあるリポジトリの記録を消去します。 + スターやお気に入りは解除されません。 + 消去 + 閲覧履歴を消去しますか? + 閲覧履歴を消去 + github.com または codeberg.org のリンクをコピーすると、開くかどうかを確認します。 + クリップボードからリポジトリのリンクを検出 + フィードと検索からミュートしたリポジトリ。 + 非表示のリポジトリ + 既読のリポジトリをフィードと検索から除外します。 + 既読のリポジトリを隠す + どの機能が使われているかを把握する手助けをしてください。 + アプリのバージョン。 + 機能の使用回数。 + 識別子は収集しません。 + リポジトリ名は収集しません。 + トークンは収集しません。 + OS とプラットフォーム。 + 匿名の使用状況データを共有 + 収集する内容 + 使用状況データ + 「%1$s」に一致する設定はありません。 + 設定を検索 + Forgejo または Gitea ホストを追加 + 追加したホスト + GitHub ミラー + デフォルトでは GitHub を検索します。地域ミラー経由でルーティングしたり、独自の Forgejo / Gitea ホストを追加したりできます。 + アプリがリポジトリを探す場所 + デフォルト(github.com) + 更新を素早く再開できるよう、インストーラーを保持しています。 + 消去 + ダウンロード済み APK + 0 B + 使用中: %1$s + この画面は進行中のリデザインの一部です。設定は引き続き動作します — 旧来のレイアウトからここへ移動している途中です。 + 今後のアップデートで実装予定 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 4a2f81736..6a90bc954 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1020,4 +1020,251 @@ Google의 개발자 인증 정책이 Android의 개방적인 배포를 위협합니다. GitHub Store는 연합을 지지합니다. 자세히 보기 배너 닫기 + + + 왼쪽의 카드를 탭하면 여기에서 세부 정보를 볼 수 있어요. + 저장소를 선택하세요 + 설치 준비 완료 + 고급 설정 + 빠른 작업 + 설정 + 상태 + 왼쪽에서 앱을 선택하면 세부 정보와 작업을 볼 수 있어요. + 앱을 선택하세요 + 설치됨 + 최신 + 저장소 열기 + 변형 선택 + 목록 숨기기 + 목록 보기 + 한 번에 검토하거나 모두 업데이트하세요. + 1개 업데이트 사용 가능 + %1$d개 업데이트 사용 가능 + 뒤로 + 뒤로 + 이미지 + 전체 보기 › + + %1$d개 호스트 + + Codeberg, gitea.com 및 git.disroot.org는 이미 기본으로 검색됩니다 — 여기에 추가할 필요가 없어요. 이 목록은 직접 호스팅하는 Forgejo 또는 Gitea에만 사용하세요. + 정보 + 개발자 + 통계 + 프로필 보기 + 새로운 기능 + 건너뛰고 나중에 검토 + 모든 신고를 확인합니다. + 받기 + #%1$d + 내 별표 저장소 + 인기 릴리스 + 가장 인기 있음 + 지금 인기 있는 항목 + 둘러보기 + 전체 보기 + 더 보기 + 토큰 추가 + 뒤로 + 취소 + 삭제 + 저장 + 토큰 추가 + 감지됨: %1$s + 포지 주소 + 비워두면 기존 토큰이 유지됩니다. 새 값을 입력하면 교체됩니다. + %1$s 토큰 교체 + %1$s에 연결됩니다 + + %1$d개 토큰 + + +를 탭하여 개인 액세스 토큰을 추가하세요 + 저장된 토큰이 없습니다 + 포지 호스트별 개인 액세스 토큰 (GitHub, Codeberg, Forgejo) + 표시 이름 (선택 사항) + 개인 액세스 토큰 + 앱을 통해 GitHub에 로그인되어 있습니다 — 권장 방법이에요. 브라우저 로그인이 불가능한 경우(제한된 네트워크, 브라우저 없음)나 GitHub 외 다른 포지에 한해 여기에 토큰을 추가하세요. + 토큰 페이지 열기 + 다른 포지 + Forgejo, Gitea, 자체 호스팅 + 토큰 붙여넣기 + 토큰 추가 대상… + 라벨 편집 + 유효하지 않음 · %1$s + 더 보기 + %1$s에서 관리 + 토큰 교체 + %1$s 남음 + 유효 · %1$s + 확인 + %1$s 토큰이 저장되었습니다 + 인증 토큰 + 실행 취소 + %1$s 토큰을 복원할 수 없습니다 + %1$s 토큰이 제거되었습니다 + 유효성 검사 실패: %1$s + 알 수 없는 오류 + 잘못된 호스트 + 호스트가 필요합니다 + 토큰이 필요합니다 + 토큰이 너무 짧습니다 + GitHub Store 정보 + 피드백 보내기… + 오픈 소스 라이선스 + 도움말 + 개인정보처리방침 + 앱 설정, 테마, 네트워크, 번역, 고급 옵션. + 일부 변경 사항을 적용하려면 다시 시작해야 합니다. + 나중에 + 언어 + 사용 데이터 + 테마 + 영향: %1$s + 지금 다시 시작 + 필터 제거 + 완료 + 필터 + 모두 초기화 + 언어 + 플랫폼 + 정렬 기준 + 출처 + 결과 필터링 + + 연결 + 설치 및 업데이트 + 모양 및 느낌 + 개인정보 및 데이터 + + %1$d개 앱 + + 저장소를 열 때 README와 릴리스 노트를 자동으로 번역합니다. 앱 언어가 대상으로 사용됩니다. + 앱 언어 따름 (%1$s) + 번역 대상 + 저장소 자동 번역 + 취소 + 언어 변경 + 감지된 원문: %1$s + 다시 시도 + 원문 보기 + 번역 보기 + 이 페이지를 다른 언어로 표시합니다. + 원문 표시 중 + %1$s(으)로 번역됨 + 번역 중… + 대상 언어 + 번역 + %1$s(으)로 번역 + 인증 키 + 무료 DeepL 키 받기 → + deepl.com/pro-api에서 가입하세요. 무료 등급 키는 :fx로 끝나며 무료 엔드포인트를 자동으로 사용합니다. Pro 등급은 텍스트를 비공개로 유지합니다. 무료 등급은 제출된 텍스트를 모델 개선에 사용할 수 있습니다. + 키 저장 + DeepL 키가 저장되었습니다 + API 키 (선택 사항) + 인스턴스 URL + 번들된 Disroot 미러를 통해 기본으로 동작합니다 — 필드를 비워두면 사용됩니다. 더 강한 프라이버시를 원하면 자체 호스팅 URL을 붙여넣으세요. API 키는 인스턴스에서 요구하는 경우에만 필요합니다. + 설정 저장 + LibreTranslate 설정이 저장되었습니다 + 무료 Azure 키 받기 → + Azure Translator는 프라이버시를 존중합니다 (기본 No-Trace — 텍스트가 저장되거나 학습에 사용되지 않습니다). 무료 등급은 월 200만 자를 제공합니다. 지역은 글로벌이 아닌 리소스에만 필요합니다. + 구독 키 + 지역 (예: westeurope, 글로벌은 비워두세요) + 자격 증명 저장 + Microsoft Translator 자격 증명이 저장되었습니다 + DeepL + LibreTranslate + Microsoft + GitHub Store + 앱에서 사용하는 라이브러리. + 오픈 소스 라이선스 + github-store.org에서 보기. + 개인정보처리방침 + 이 앱의 소스 보기. + GitHub의 소스 코드 + GitHub, Codeberg, Forgejo 릴리스를 위한 크로스 플랫폼 앱 스토어. + 지난 릴리스 노트. + 새로운 기능 + 아래에서 재정의하지 않는 한 모든 트래픽에 적용됩니다. + 사용자 지정 + 아래에서 연결 모드를 선택하세요. 대부분의 사용자는 프록시 없음으로 둡니다. + 앱이 인터넷에 연결되는 방식 + 주 연결 + HTTP/HTTPS + 대부분의 기업용 프록시. + 프록시 없음 + SOCKS5 + Tor, SSH 터널. + 시스템 + 각 스코프는 기본적으로 주 연결을 사용합니다. 자체 설정을 유지하려면 \'사용자 지정\'을 선택하세요. + 스코프별 재정의 + 전체 URL 붙여넣기 + 전체 프록시 URL을 붙여넣으면 양식을 자동으로 채워 드립니다. + 이 URL 사용 + URL을 읽을 수 없었어요. + scheme://user:pass@host:port + 프록시 URL 붙여넣기 + GitHub API, 검색, 저장소 세부 정보. + 검색 및 메타데이터 + APK 및 에셋 다운로드. + 다운로드 + DeepL, Microsoft, LibreTranslate 호출. + 번역 + 테스트 + 주 연결 사용 + 액세스 토큰 + 앱 정보 + 모양 + 연결 + 피드백 보내기 + 설치 방법 + 언어 + 개인정보 + 출처 + 저장소 + 관리하려면 탭하세요 + 번역 + 업데이트 동작 + 사일런트 설치 프로그램과 설치 프로그램 출처 설정은 Android 전용 기능입니다. 데스크톱 설치는 OS 패키지 관리자를 통해 이루어집니다. + 설치 방법은 Android 전용입니다 + 언어를 전환하면 앱이 다시 시작됩니다. + 항목을 탭하면 해당 프로젝트 페이지가 열립니다. + GitHub Store는 이 라이브러리들에 의지하고 있어요. + 오픈 소스 라이선스 + 탐색 기록 + 이미 열어본 저장소 기록을 잊습니다. + 별표나 즐겨찾기는 해제되지 않습니다. + 지우기 + 조회 기록을 지우시겠어요? + 조회 기록 지우기 + github.com 또는 codeberg.org 링크를 복사하면 열도록 안내합니다. + 클립보드에서 저장소 링크 감지 + 피드와 검색에서 숨긴 저장소입니다. + 숨긴 저장소 + 피드와 검색에서 본 저장소를 건너뜁니다. + 이미 본 저장소 숨기기 + 어떤 기능이 사용되는지 파악하는 데 도움을 주세요. + 앱 버전. + 기능 사용 횟수. + 식별자 없음. + 저장소 이름 없음. + 토큰 없음. + OS 및 플랫폼. + 익명 사용 데이터 공유 + 수집 항목 + 사용 데이터 + \'%1$s\'와(과) 일치하는 설정이 없습니다. + 설정 검색 + Forgejo 또는 Gitea 호스트 추가 + 추가된 호스트 + GitHub 미러 + 앱은 기본적으로 GitHub를 검색합니다. 지역 미러를 통해 라우팅하거나 사용자 지정 Forgejo / Gitea 호스트를 추가하세요. + 앱이 저장소를 찾는 위치 + 기본 (github.com) + 업데이트를 빠르게 재개할 수 있도록 설치 파일을 보관합니다. + 지우기 + 다운로드된 APK + 0 B + 사용 중: %1$s + 이 화면은 진행 중인 디자인 개편의 일부입니다. 설정은 정상 동작하며 — 이전 레이아웃에서 이곳으로 옮겨지는 중일 뿐입니다. + 후속 업데이트 예정 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index cc3de13a1..1f9efa336 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1017,4 +1017,260 @@ Polityka weryfikacji deweloperów Google zagraża otwartej dystrybucji Androida. GitHub Store popiera koalicję. Dowiedz się więcej Zamknij baner + + + Dotknij dowolnej karty po lewej, aby zobaczyć jej szczegóły tutaj. + Wybierz repozytorium + Gotowe do instalacji + Ustawienia zaawansowane + Szybkie akcje + Ustawienia + Status + Wybierz dowolną aplikację po lewej, aby zobaczyć jej szczegóły i akcje. + Wybierz aplikację + Zainstalowana + Najnowsza + Otwórz repozytorium + Wybierz wariant + Ukryj listę + Zobacz listę + Przejrzyj lub zaktualizuj wszystko naraz. + 1 dostępna aktualizacja + Dostępnych aktualizacji: %1$d + Wróć + Wróć + Obraz + Zobacz wszystkie › + + %1$d host + %1$d hosty + %1$d hostów + %1$d hostów + + Codeberg, gitea.com i git.disroot.org są już domyślnie przeszukiwane — nie musisz ich tu dodawać. Tej listy używaj tylko dla własnych, samodzielnie hostowanych instancji Forgejo lub Gitea. + Informacje + Deweloper + Statystyki + Zobacz profil + Co nowego + Pomiń i przejrzyj później + Czytamy każdą wiadomość. + Pobierz + #%1$d + Z Twoich gwiazdek + Gorące wydania + Najpopularniejsze + Na czasie + Odkrywaj + Zobacz wszystkie + Zobacz więcej + Dodaj token + Wróć + Anuluj + Usuń + Zapisz + Dodaj token + Wykryto: %1$s + Adres forge + Pozostaw puste, aby zachować istniejący token. Wpisz nową wartość, aby go zastąpić. + Zastąp token dla %1$s + Połączy się z %1$s + + %1$d token + %1$d tokeny + %1$d tokenów + %1$d tokenów + + Dotknij +, aby dodać Personal Access Token + Brak zapisanych tokenów + Personal Access Tokens dla poszczególnych hostów forge (GitHub, Codeberg, Forgejo) + Nazwa wyświetlana (opcjonalnie) + Personal Access Token + Jesteś zalogowany na GitHub w aplikacji — to zalecana ścieżka. Dodaj token tutaj tylko jeśli logowanie przez przeglądarkę nie zadziała (restrykcyjna sieć, brak przeglądarki) lub dla forge innych niż GitHub. + Otwórz stronę tokenów + Inny forge + Forgejo, Gitea, własny hosting + Wklej token + Dodaj token dla… + Edytuj etykietę + Nieprawidłowy · %1$s + Więcej + Zarządzaj na %1$s + Zastąp token + pozostało %1$s + Prawidłowy · %1$s + Sprawdź + Zapisano token dla %1$s + Tokeny uwierzytelniania + Cofnij + Nie udało się przywrócić tokena dla %1$s + Usunięto token dla %1$s + Weryfikacja nie powiodła się: %1$s + nieznany błąd + Nieprawidłowy host + Host jest wymagany + Token jest wymagany + Token zbyt krótki + O GitHub Store + Wyślij opinię… + Licencje open source + Pomoc + Polityka prywatności + Ustawienia aplikacji, motyw, sieć, tłumaczenie, opcje zaawansowane. + Niektóre zmiany wymagają ponownego uruchomienia, aby zostały zastosowane. + Później + język + dane użycia + motyw + Dotyczy: %1$s + Uruchom ponownie teraz + Usuń filtr + Gotowe + Filtry + Zresetuj wszystko + Język + Platforma + Sortuj według + Źródło + Filtruj wyniki + Aplikacja + Łączność + Instalacje i aktualizacje + Wygląd + Prywatność i dane + + %1$d aplikacja + %1$d aplikacje + %1$d aplikacji + %1$d aplikacji + + Tłumacz automatycznie README i informacje o wydaniu przy otwieraniu repozytorium. Używa języka aplikacji jako docelowego. + Zgodnie z językiem aplikacji (%1$s) + Tłumacz na + Automatyczne tłumaczenie repozytoriów + Anuluj + Zmień język + Wykryte źródło: %1$s + Ponów + Pokaż oryginał + Pokaż tłumaczenie + Wyświetl tę stronę w innym języku. + Pokazuję oryginał + Przetłumaczono na %1$s + Tłumaczenie… + Język docelowy + Przetłumacz + Przetłumacz na %1$s + Klucz uwierzytelniający + Zdobądź darmowy klucz DeepL → + Zarejestruj się na deepl.com/pro-api. Klucze darmowego planu kończą się na :fx i automatycznie używają darmowego endpointu. Plan Pro nie udostępnia Twojego tekstu; w planie darmowym przesłany tekst może być wykorzystany do trenowania modeli. + Zapisz klucz + Klucz DeepL zapisany + Klucz API (opcjonalnie) + URL instancji + Działa od ręki przez publiczny serwer lustrzany Disroot — pozostaw pola puste, aby z niego korzystać. Aby zwiększyć prywatność, wklej własny adres samodzielnego hostingu. Klucz API potrzebny tylko, jeśli instancja go wymaga. + Zapisz ustawienia + Ustawienia LibreTranslate zapisane + Zdobądź darmowy klucz Azure → + Azure Translator szanuje prywatność (domyślnie No-Trace — Twój tekst nie jest przechowywany ani używany do trenowania). Darmowy plan obejmuje 2 mln znaków miesięcznie. Region wymagany tylko dla zasobów innych niż globalne. + Klucz subskrypcji + Region (np. westeurope, pozostaw puste dla Global) + Zapisz poświadczenia + Poświadczenia Microsoft Translator zapisane + DeepL + LibreTranslate + Microsoft + GitHub Store + Biblioteki używane w aplikacji. + Licencje open source + Zobacz na github-store.org. + Polityka prywatności + Zobacz źródło tej aplikacji. + Kod źródłowy na GitHub + Wieloplatformowy sklep z aplikacjami dla wydań GitHub, Codeberg i Forgejo. + Wcześniejsze informacje o wydaniach. + Co nowego + Dotyczy całego ruchu, chyba że zostanie zastąpione poniżej. + Niestandardowe + Wybierz tryb połączenia poniżej. Większość użytkowników pozostawia opcję Bez proxy. + Jak aplikacja łączy się z internetem + Główne połączenie + HTTP/HTTPS + Większość korporacyjnych proxy. + Bez proxy + SOCKS5 + Tor, tunele SSH. + Systemowe + Każdy zakres domyślnie używa głównego połączenia. Wybierz \'Niestandardowe\', aby zachować własne ustawienia. + Nadpisania per-zakres + Wklej pełny URL + Wklej pełny URL proxy, a wypełnimy formularz za Ciebie. + Użyj tego URL + Nie udało się odczytać tego URL. + scheme://user:pass@host:port + Wklej URL proxy + API GitHub, wyszukiwanie, szczegóły repozytorium. + Wyszukiwanie i metadane + Pobieranie APK i zasobów. + Pobieranie + Wywołania DeepL, Microsoft, LibreTranslate. + Tłumaczenie + Testuj + Użyj głównego + Tokeny dostępu + O aplikacji + Wygląd + Połączenie + Wyślij opinię + Metoda instalacji + Język + Prywatność + Źródła + Pamięć + Dotknij, aby zarządzać + Tłumaczenie + Zachowanie aktualizacji + Ciche instalatory i atrybucja instalatora to funkcje wyłącznie dla Androida. Instalacje na komputerze przechodzą przez menedżera pakietów systemu. + Metoda instalacji dostępna tylko na Androidzie + Aplikacja uruchamia się ponownie po zmianie języka. + Dotknij dowolnego wpisu, aby otworzyć stronę projektu. + GitHub Store opiera się na tych bibliotekach. + Licencje open source + Historia przeglądania + Zapomnij, które repozytoria zostały już otwarte. + To nie usunie gwiazdek ani ulubionych. + Wyczyść + Wyczyścić historię przeglądania? + Wyczyść historię przeglądania + Gdy skopiujesz link github.com lub codeberg.org, zaproponujemy jego otwarcie. + Wykrywaj linki do repo w schowku + Repozytoria wyciszone w kanałach i wyszukiwarce. + Ukryte repozytoria + Pomijaj obejrzane repo w kanałach i wynikach wyszukiwania. + Ukrywaj repo, które już widziałem + Pomóż nam zrozumieć, z których funkcji się korzysta. + Wersja aplikacji. + Liczniki użycia funkcji. + Bez identyfikatorów. + Bez nazw repozytoriów. + Bez tokenów. + System i platforma. + Udostępniaj anonimowe dane użycia + Co zbieramy + Dane użycia + Żadne ustawienie nie pasuje do \'%1$s\'. + Szukaj ustawień + Dodaj host Forgejo lub Gitea + Dodane hosty + Serwer lustrzany GitHub + Aplikacja domyślnie przeszukuje GitHub. Możesz kierować ruch przez regionalny serwer lustrzany lub dodać własne hosty Forgejo / Gitea. + Gdzie aplikacja szuka repozytoriów + Domyślny (github.com) + Trzymamy instalatory, aby aktualizacje były szybsze. + Wyczyść + Pobrane pliki APK + 0 B + Zajmuje: %1$s + Ten ekran jest częścią trwającego przeprojektowania. Ustawienia nadal działają — są tylko przenoszone tutaj ze starego układu. + Wkrótce w kolejnej aktualizacji diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index b584e442f..f8698b534 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1017,4 +1017,260 @@ Политика верификации разработчиков Google угрожает открытому распространению Android. GitHub Store поддерживает коалицию. Подробнее Закрыть баннер + + + Нажми на любую карточку слева, чтобы увидеть детали здесь. + Выбери репозиторий + Готово к установке + Расширенные настройки + Быстрые действия + Настройки + Статус + Выбери любое приложение слева, чтобы увидеть его детали и действия. + Выбери приложение + Установлено + Последняя + Открыть репозиторий + Выбрать вариант + Скрыть список + Показать список + Просмотри или обнови всё сразу. + Доступно 1 обновление + Доступно обновлений: %1$d + Назад + Назад + Изображение + Все › + + %1$d хост + %1$d хоста + %1$d хостов + %1$d хоста + + Codeberg, gitea.com и git.disroot.org уже доступны по умолчанию — добавлять их сюда не нужно. Используй этот список только для собственного Forgejo или Gitea. + О репозитории + Разработчик + Статистика + Профиль + Что нового + Пропустить и посмотреть позже + Мы читаем каждый отзыв. + Получить + #%1$d + Из ваших избранных + Свежие релизы + Самые популярные + В тренде + Обзор + Показать все + Показать ещё + Добавить токен + Назад + Отмена + Удалить + Сохранить + Добавить токен + Определено: %1$s + Адрес форджа + Оставь пустым, чтобы сохранить текущий токен. Введи новое значение, чтобы заменить его. + Заменить токен для %1$s + Подключение к %1$s + + %1$d токен + %1$d токена + %1$d токенов + %1$d токена + + Нажми +, чтобы добавить персональный токен доступа + Токены не сохранены + Персональные токены для каждого хоста (GitHub, Codeberg, Forgejo) + Отображаемое имя (необязательно) + Персональный токен доступа + Ты уже вошёл в GitHub через приложение — это рекомендуемый путь. Добавляй токен сюда, только если вход через браузер не работает (ограниченная сеть, нет браузера) или для других форджей, кроме GitHub. + Открыть страницу токена + Другой фордж + Forgejo, Gitea, собственный + Вставить токен + Добавить токен для… + Изменить метку + Недействителен · %1$s + Ещё + Управлять на %1$s + Заменить токен + Осталось %1$s + Действителен · %1$s + Проверить + Токен сохранён для %1$s + Токены аутентификации + Отменить + Не удалось восстановить токен для %1$s + Токен для %1$s удалён + Проверка не пройдена: %1$s + неизвестная ошибка + Неверный хост + Требуется хост + Требуется токен + Токен слишком короткий + О GitHub Store + Отправить отзыв… + Лицензии открытого ПО + Помощь + Политика конфиденциальности + Настройки приложения, тема, сеть, перевод, расширенные параметры. + Некоторым изменениям нужен перезапуск. + Позже + язык + данные использования + тема + Затронуто: %1$s + Перезапустить + Убрать фильтр + Готово + Фильтры + Сбросить + Язык + Платформа + Сортировка + Источник + Фильтр результатов + Приложение + Подключение + Установка и обновления + Внешний вид + Приватность и данные + + %1$d приложение + %1$d приложения + %1$d приложений + %1$d приложения + + Автоматически переводить README и заметки о выпуске при открытии репозитория. В качестве цели используется язык приложения. + Следовать языку приложения (%1$s) + Переводить на + Автоперевод репозиториев + Отмена + Сменить язык + Исходный язык: %1$s + Повторить + Показать оригинал + Показать перевод + Покажи эту страницу на другом языке. + Показан оригинал + Переведено на %1$s + Перевод… + Язык перевода + Перевод + Перевести на %1$s + Ключ авторизации + Получить бесплатный ключ DeepL → + Зарегистрируйся на deepl.com/pro-api. Ключи бесплатного тарифа оканчиваются на :fx и автоматически используют бесплатный эндпоинт. Pro-тариф сохраняет приватность текста; Free-тариф может использовать отправленный текст для улучшения моделей. + Сохранить ключ + Ключ DeepL сохранён + API-ключ (необязательно) + URL инстанса + Работает «из коробки» через публичное зеркало Disroot — оставь поля пустыми, чтобы использовать его. Для большей приватности укажи URL своего инстанса. API-ключ нужен, только если этого требует инстанс. + Сохранить настройки + Настройки LibreTranslate сохранены + Получить бесплатный ключ Azure → + Azure Translator уважает приватность (No-Trace по умолчанию — текст не сохраняется и не используется для обучения). Бесплатный тариф — до 2M символов в месяц. Регион нужен только для не-глобальных ресурсов. + Ключ подписки + Регион (например, westeurope, оставь пустым для Global) + Сохранить учётные данные + Учётные данные Microsoft Translator сохранены + DeepL + LibreTranslate + Microsoft + GitHub Store + Библиотеки, используемые в приложении. + Лицензии открытого ПО + Открыть на github-store.org. + Политика конфиденциальности + Посмотреть исходный код приложения. + Исходный код на GitHub + Кросс-платформенный магазин приложений для релизов GitHub, Codeberg и Forgejo. + Заметки прошлых релизов. + Что нового + Применяется ко всему трафику, если ниже не задано иное. + Своё + Выбери режим подключения ниже. Большинству подойдёт «Без прокси». + Как приложение выходит в интернет + Основное подключение + HTTP/HTTPS + Большинство корпоративных прокси. + Без прокси + SOCKS5 + Tor, SSH-туннели. + Системный + Каждая область по умолчанию использует основное подключение. Выбери «Своё», чтобы задать отдельные настройки. + Переопределения по областям + Вставить полный URL + Вставь полный URL прокси, и мы заполним форму за тебя. + Использовать этот URL + Не удалось прочитать этот URL. + scheme://user:pass@host:port + Вставить URL прокси + API GitHub, поиск, детали репозитория. + Поиск и метаданные + Загрузка APK и ассетов. + Загрузки + Запросы к DeepL, Microsoft, LibreTranslate. + Перевод + Проверить + Как основное + Токены доступа + О приложении + Внешний вид + Подключение + Отправить отзыв + Способ установки + Язык + Приватность + Источники + Хранилище + Нажми, чтобы настроить + Перевод + Обновления + Тихие установщики и атрибуция установщика — это функции Android. На десктопе установка идёт через системный менеджер пакетов. + Способ установки доступен только на Android + Приложение перезапустится при смене языка. + Нажми на любую запись, чтобы открыть её страницу проекта. + GitHub Store построен на этих библиотеках. + Лицензии открытого ПО + История просмотров + Забыть, какие репо ты уже открывал. + Это не уберёт звёзды и не снимет с избранного. + Очистить + Очистить историю просмотров? + Очистить историю просмотров + Когда ты копируешь ссылку на github.com или codeberg.org, мы предложим её открыть. + Замечать ссылки на репо в буфере обмена + Репо, которые ты убрал из ленты и поиска. + Скрытые репозитории + Пропускать просмотренные репо в ленте и поиске. + Скрывать уже просмотренные репо + Помоги нам понять, какие функции используются. + Версия приложения. + Счётчики использования функций. + Никаких идентификаторов. + Никаких названий репо. + Никаких токенов. + ОС и платформа. + Делиться анонимными данными использования + Что мы собираем + Данные использования + Под «%1$s» ничего не нашлось. + Поиск по настройкам + Добавить хост Forgejo или Gitea + Добавленные хосты + Зеркало GitHub + По умолчанию приложение ищет на GitHub. Можно направить через региональное зеркало или добавить свои хосты Forgejo / Gitea. + Где приложение ищет репозитории + По умолчанию (github.com) + Установщики остаются на диске, чтобы обновления возобновлялись быстро. + Очистить + Загруженные APK + 0 Б + Занято: %1$s + Этот экран — часть продолжающегося редизайна. Сами настройки работают — их просто переносят сюда со старого экрана. + Появится в следующем обновлении diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 16da4369d..662e47216 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1027,4 +1027,258 @@ Google'ın geliştirici doğrulama politikası açık Android dağıtımını tehdit ediyor. GitHub Store koalisyonu destekliyor. Daha fazla bilgi Banner'ı kapat + + + + %1$d host + %1$d host + + + %1$d repo + %1$d repo + + + %1$d token + %1$d token + + + %1$d uygulama + %1$d uygulama + + Ayrıntılarını burada görüntülemek için soldaki herhangi bir karta dokunun. + Bir depo seçin + Kurulmaya hazır + Gelişmiş ayarlar + Hızlı işlemler + Ayarlar + Durum + Ayrıntılarını ve işlemlerini görmek için soldaki herhangi bir uygulamayı seçin. + Bir uygulama seçin + Yüklü + En son + Depoyu aç + Varyant seç + Listeyi gizle + Listeyi gör + Tümünü tek seferde incele veya güncelle. + 1 güncelleme mevcut + %1$d güncelleme mevcut + Geri + Geri + Resim + Tümünü gör › + Codeberg, gitea.com ve git.disroot.org zaten varsayılan olarak aranır — bunları buraya eklemenize gerek yok. Bu listeyi yalnızca kendi self-hosted Forgejo veya Gitea örneğiniz için kullanın. + Hakkında + Geliştirici + İstatistikler + Profili gör + Neler yeni + Atla ve sonra incele + Her raporu okuyoruz. + Al + #%1$d + Yıldızlılarınızdan + Popüler sürümler + En popüler + Şu anda trend + Keşfet + Tümünü gör + Daha fazla gör + Token ekle + Geri + İptal + Sil + Kaydet + Token ekle + Algılandı: %1$s + Forge adresi + Mevcut tokeni korumak için boş bırakın. Değiştirmek için yeni bir değer girin. + %1$s için tokeni değiştir + %1$s adresine bağlanacak + Bir personal access token eklemek için + simgesine dokunun + Kaydedilmiş token yok + Her forge hostu için personal access token (GitHub, Codeberg, Forgejo) + Görünen ad (isteğe bağlı) + Personal access token + Uygulamadan GitHub\'a giriş yaptınız — bu önerilen yoldur. Buraya yalnızca tarayıcı girişi çalışmıyorsa (kısıtlı ağ, tarayıcı yok) veya GitHub dışındaki forgeler için token ekleyin. + Token sayfasını aç + Diğer forge + Forgejo, Gitea, self-hosted + Token yapıştır + Şuna token ekle… + Etiketi düzenle + Geçersiz · %1$s + Daha fazla + %1$s üzerinde yönet + Tokeni değiştir + %1$s kaldı + Geçerli · %1$s + Kontrol et + %1$s için token kaydedildi + Kimlik doğrulama tokenleri + Geri al + %1$s için token geri yüklenemedi + %1$s için token kaldırıldı + Doğrulama başarısız: %1$s + bilinmeyen hata + Geçersiz host + Host gerekli + Token gerekli + Token çok kısa + GitHub Store Hakkında + Geri bildirim gönder… + Açık kaynak lisansları + Yardım + Gizlilik politikası + Uygulama ayarları, tema, ağ, çeviri, gelişmiş seçenekler. + Bazı değişikliklerin uygulanması için yeniden başlatma gerekir. + Sonra + dil + kullanım verisi + tema + Etkilenen: %1$s + Şimdi yeniden başlat + Filtreyi kaldır + Tamam + Filtreler + Tümünü sıfırla + Dil + Platform + Sırala + Kaynak + Sonuçları filtrele + Uygulama + Bağlantı + Kurulum ve güncellemeler + Görünüm + Gizlilik ve veri + Bir depo açıldığında README ve sürüm notları otomatik çevrilir. Uygulama dilinizi hedef olarak kullanır. + Uygulama dilini izle (%1$s) + Şuna çevir + Depoları otomatik çevir + İptal + Dili değiştir + Algılanan kaynak: %1$s + Tekrar dene + Orijinali göster + Çeviriyi göster + Bu sayfayı başka bir dilde göster. + Orijinal gösteriliyor + %1$s diline çevrildi + Çevriliyor… + Hedef dil + Çevir + %1$s diline çevir + Yetkilendirme anahtarı + Ücretsiz DeepL anahtarı alın → + deepl.com/pro-api adresinden kaydolun. Ücretsiz anahtarlar :fx ile biter ve otomatik olarak ücretsiz uç noktayı kullanır. Pro katmanı metninizi gizli tutar; Ücretsiz katman gönderilen metni model geliştirmek için kullanabilir. + Anahtarı kaydet + DeepL anahtarı kaydedildi + API anahtarı (isteğe bağlı) + Sunucu URL\'si + Public Disroot aynası aracılığıyla kutudan çıkar çıkmaz çalışır — kullanmak için alanları boş bırakın. Daha fazla gizlilik için kendi self-hosted URL\'nizi yapıştırın. API anahtarı yalnızca sunucu istiyorsa gereklidir. + Ayarları kaydet + LibreTranslate ayarları kaydedildi + Ücretsiz Azure anahtarı alın → + Azure Translator gizliliğe saygılıdır (varsayılan olarak No-Trace — metniniz asla saklanmaz veya eğitim için kullanılmaz). Ücretsiz katman aylık 2M karakteri kapsar. Bölge yalnızca global olmayan kaynaklar için gereklidir. + Abonelik anahtarı + Bölge (ör. westeurope, Global için boş bırakın) + Kimlik bilgilerini kaydet + Microsoft Translator kimlik bilgileri kaydedildi + DeepL + LibreTranslate + Microsoft + GitHub Store + Uygulamada kullanılan kütüphaneler. + Açık kaynak lisansları + github-store.org üzerinde gör. + Gizlilik politikası + Bu uygulamanın kaynağını gör. + GitHub\'da kaynak kodu + GitHub, Codeberg ve Forgejo sürümleri için çapraz platform uygulama mağazası. + Geçmiş sürüm notları. + Neler yeni + Aşağıdan geçersiz kılınmadıkça tüm trafiğe uygulanır. + Özel + Aşağıdan bir bağlantı modu seçin. Çoğu kullanıcı bunu Proxy yok olarak bırakır. + Uygulama internete nasıl ulaşır + Ana bağlantı + HTTP/HTTPS + Çoğu kurumsal proxy. + Proxy yok + SOCKS5 + Tor, SSH tünelleri. + Sistem + Her kapsam varsayılan olarak ana bağlantıyı kullanır. Kendi ayarlarını korumak için \'Özel\'i seçin. + Kapsama göre geçersiz kılmalar + Tam URL yapıştır + Tam bir proxy URL\'si yapıştırın, formu sizin için dolduralım. + Bu URL\'yi kullan + Bu URL okunamadı. + scheme://user:pass@host:port + Proxy URL\'sini yapıştır + GitHub API, arama, repo ayrıntıları. + Arama ve meta veri + APK ve dosya indirmeleri. + İndirmeler + DeepL, Microsoft, LibreTranslate çağrıları. + Çeviri + Test + Ana bağlantıyı kullan + Erişim tokenleri + Uygulama bilgisi + Görünüm + Bağlantı + Geri bildirim gönder + Kurulum yöntemi + Dil + Gizlilik + Kaynaklar + Depolama + Yönetmek için dokunun + Çeviri + Güncelleme davranışı + Sessiz yükleyiciler ve yükleyici atfı yalnızca Android özellikleridir. Masaüstü kurulumlar işletim sistemi paket yöneticisi üzerinden yapılır. + Kurulum yöntemi yalnızca Android + Dili değiştirdiğinizde uygulama yeniden başlar. + Proje sayfasını açmak için herhangi bir girişe dokunun. + GitHub Store bu kütüphaneler üzerinde yükselir. + Açık kaynak lisansları + Tarama geçmişi + Hangi repoları açtığınızı unut. + Bu işlem hiçbir şeyin yıldızını veya favorisini kaldırmaz. + Temizle + Görüntüleme geçmişi temizlensin mi? + Görüntüleme geçmişini temizle + github.com veya codeberg.org bağlantısı kopyaladığınızda, açmanız için size soracağız. + Panodaki repo bağlantılarını algıla + Akışlardan ve aramadan sessize aldığınız repolar. + Gizli depolar + Akışlarda ve aramada görülen repoları atla. + Daha önce görüntülediğim repoları gizle + Hangi özelliklerin kullanıldığını anlamamıza yardımcı ol. + Uygulama sürümü. + Özellik kullanım sayıları. + Tanımlayıcı yok. + Repo adı yok. + Token yok. + İşletim sistemi ve platform. + Anonim kullanım verisi paylaş + Ne topluyoruz + Kullanım verisi + \'%1$s\' ile eşleşen ayar yok. + Ayarları ara + Bir Forgejo veya Gitea hostu ekle + Eklenen hostlar + GitHub aynası + Uygulama varsayılan olarak GitHub\'da arama yapar. Bölgesel bir aynaya yönlendirin veya özel Forgejo / Gitea hostları ekleyin. + Uygulama repoları nerede arar + Varsayılan (github.com) + Güncellemelerin hızlı devam etmesi için yükleyicileri saklarız. + Temizle + İndirilen APK\'lar + 0 B + Kullanılıyor: %1$s + Bu ekran sürmekte olan yeniden tasarımın bir parçası. Ayarlar hâlâ çalışıyor — sadece eski düzenden buraya taşınıyor. + Yakında bir sonraki sürümde diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 57ed23268..af263bc98 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -993,4 +993,251 @@ Google 的开发者验证政策威胁 Android 的开放分发。GitHub Store 与联盟同行。 了解更多 关闭横幅 + + + 点按左侧任意卡片,在此查看详情。 + 选择一个仓库 + 可以安装 + 高级设置 + 快捷操作 + 设置 + 状态 + 在左侧选择任意应用,即可查看详情和操作。 + 选择一个应用 + 已安装 + 最新 + 打开仓库 + 选择变体 + 隐藏列表 + 查看列表 + 逐个审查或一键全部更新。 + 1 个可用更新 + %1$d 个可用更新 + 返回 + 返回 + 图片 + 查看全部 › + + %1$d 个主机 + + Codeberg、gitea.com 和 git.disroot.org 默认已被搜索 — 无需在此添加。此列表仅用于你自己的 Forgejo 或 Gitea 自建实例。 + 关于 + 开发者 + 统计 + 查看资料 + 更新内容 + 跳过,稍后查看 + 每一条反馈我们都会阅读。 + 获取 + #%1$d + 来自你的星标 + 热门发布 + 最受欢迎 + 当下流行 + 发现 + 查看全部 + 查看更多 + 添加令牌 + 返回 + 取消 + 删除 + 保存 + 添加令牌 + 检测到:%1$s + 代码托管地址 + 留空可保留现有令牌。输入新值以替换。 + 替换 %1$s 的令牌 + 将连接到 %1$s + + %1$d 个令牌 + + 点按 + 添加个人访问令牌 + 尚未保存任何令牌 + 每个代码托管站点的个人访问令牌(GitHub、Codeberg、Forgejo) + 显示名称(可选) + 个人访问令牌 + 你已通过应用使用 GitHub 登录 — 这是推荐方式。仅在浏览器登录无法使用(受限网络、无浏览器)或针对 GitHub 之外的代码托管站点时,才在此添加令牌。 + 打开令牌页面 + 其他代码托管 + Forgejo、Gitea、自建实例 + 粘贴令牌 + 为以下站点添加令牌… + 编辑标签 + 无效 · %1$s + 更多 + 在 %1$s 上管理 + 替换令牌 + 剩余 %1$s + 有效 · %1$s + 检查 + 已保存 %1$s 的令牌 + 认证令牌 + 撤销 + 无法恢复 %1$s 的令牌 + 已移除 %1$s 的令牌 + 验证失败:%1$s + 未知错误 + 无效的主机 + 必须填写主机 + 必须填写令牌 + 令牌太短 + 关于 GitHub Store + 发送反馈… + 开源许可证 + 帮助 + 隐私政策 + 应用设置、主题、网络、翻译、高级选项。 + 部分更改需要重启后生效。 + 稍后 + 语言 + 使用数据 + 主题 + 受影响:%1$s + 立即重启 + 移除筛选 + 完成 + 筛选 + 全部重置 + 语言 + 平台 + 排序方式 + 来源 + 筛选结果 + 应用 + 连接 + 安装与更新 + 外观与显示 + 隐私与数据 + + %1$d 个应用 + + 打开仓库时自动翻译 README 和发布说明。以应用语言为目标语言。 + 跟随应用语言(%1$s) + 翻译为 + 自动翻译仓库 + 取消 + 更改语言 + 检测到的源语言:%1$s + 重试 + 显示原文 + 显示译文 + 将此页面渲染为其他语言。 + 显示原文 + 已翻译为 %1$s + 正在翻译… + 目标语言 + 翻译 + 翻译为 %1$s + Auth 密钥 + 获取免费 DeepL 密钥 → + 在 deepl.com/pro-api 注册。免费版密钥以 :fx 结尾,会自动使用免费端点。专业版会保持你的文本私密;免费版可能使用提交的文本来改进模型。 + 保存密钥 + DeepL 密钥已保存 + API 密钥(可选) + 实例 URL + 通过公共 Disroot 镜像开箱即用 — 留空字段即可使用。为获得更高隐私性,可粘贴自建 URL。仅当实例要求时才需要 API 密钥。 + 保存设置 + LibreTranslate 设置已保存 + 获取免费 Azure 密钥 → + Azure Translator 注重隐私(默认 No-Trace — 你的文本从不存储,也不用于训练)。免费版每月可翻译 200 万字符。仅非全球资源需要填写区域。 + 订阅密钥 + 区域(例如 westeurope,全球留空) + 保存凭据 + Microsoft Translator 凭据已保存 + DeepL + LibreTranslate + Microsoft + GitHub Store + 应用使用的开源库。 + 开源许可证 + 在 github-store.org 上查看。 + 隐私政策 + 查看此应用的源代码。 + GitHub 上的源代码 + 面向 GitHub、Codeberg 和 Forgejo 发布版的跨平台应用商店。 + 过往发布说明。 + 更新内容 + 除非下方覆盖,否则适用于全部流量。 + 自定义 + 从下方选择一种连接模式。大多数人保持\"不使用代理\"即可。 + 应用如何连接到互联网 + 主连接 + HTTP/HTTPS + 大多数企业代理。 + 不使用代理 + SOCKS5 + Tor、SSH 隧道。 + 系统 + 每个范围默认使用主连接。选择\"自定义\"以保留各自的设置。 + 分范围覆盖 + 粘贴完整 URL + 粘贴完整代理 URL,我们会自动填入表单。 + 使用此 URL + 无法解析该 URL。 + scheme://user:pass@host:port + 粘贴代理 URL + GitHub API、搜索、仓库详情。 + 搜索与元数据 + APK 和资源下载。 + 下载 + DeepL、Microsoft、LibreTranslate 请求。 + 翻译 + 测试 + 使用主连接 + 访问令牌 + 应用信息 + 外观 + 连接 + 发送反馈 + 安装方式 + 语言 + 隐私 + 来源 + 存储 + 点按管理 + 翻译 + 更新行为 + 静默安装器和安装器归属仅限 Android。桌面端安装通过系统包管理器完成。 + 安装方式仅限 Android + 切换语言时应用会重启。 + 点按任意条目以打开其项目页面。 + GitHub Store 依托于这些开源库。 + 开源许可证 + 浏览记录 + 忘记你打开过哪些仓库。 + 这不会取消任何星标或收藏。 + 清除 + 清除浏览记录? + 清除浏览记录 + 当你复制 github.com 或 codeberg.org 链接时,我们会提示你打开。 + 检测剪贴板中的仓库链接 + 你从信息流和搜索中静音的仓库。 + 已隐藏的仓库 + 在信息流和搜索中跳过已查看的仓库。 + 隐藏我已查看过的仓库 + 帮助我们了解哪些功能被使用。 + 应用版本。 + 功能使用计数。 + 不收集标识符。 + 不收集仓库名。 + 不收集令牌。 + 操作系统和平台。 + 共享匿名使用数据 + 我们收集的内容 + 使用数据 + 没有设置匹配 \'%1$s\'。 + 搜索设置 + 添加 Forgejo 或 Gitea 主机 + 已添加的主机 + GitHub 镜像 + 应用默认搜索 GitHub。可通过区域镜像路由,或添加自定义的 Forgejo / Gitea 主机。 + 应用从哪里查找仓库 + 默认 (github.com) + 我们会保留安装包,以便更新可以快速继续。 + 清除 + 已下载的 APK + 0 B + 占用:%1$s + 此页面属于正在进行的重新设计。设置依然有效 — 只是正从旧布局迁移到这里。 + 后续更新中 From a3e0a928485ee5b2aefe7209aef32320d0d90872 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 16:38:06 +0500 Subject: [PATCH 123/172] =?UTF-8?q?i18n(ru):=20normalize=20to=20informal?= =?UTF-8?q?=20=D1=82=D1=8B=20register?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composeResources/values-ru/strings-ru.xml | 223 +++++++++--------- 1 file changed, 114 insertions(+), 109 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index f8698b534..ea6c32212 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -30,20 +30,20 @@ Ожидание авторизации… Вход выполнен! - Теперь вы можете использовать приложение. Перенаправление… + Теперь ты можешь использовать приложение. Перенаправление… Попробовать снова Ошибка: %1$s - Введите этот код на GitHub: + Введи этот код на GitHub: Скопировать код Открыть GitHub - Откройте полный\nдоступ + Открой полный\nдоступ Больше запросов - Войдите, чтобы получить более высокий лимит API и избежать прерываний. + Войди, чтобы получить более высокий лимит API и избежать прерываний. Войти через GitHub @@ -94,7 +94,7 @@ Профиль Как в системе - Перезапустите, чтобы применить новый язык. + Перезапусти, чтобы применить новый язык. Перезапустить Цвет темы @@ -104,10 +104,10 @@ Выйти - Вы успешно вышли, перенаправление... + Ты успешно вышел, перенаправление... Внимание! - Вы уверены, что хотите выйти? + Точно хочешь выйти? Лесная Nord @@ -151,7 +151,7 @@ Найти ещё на GitHub Поиск на GitHub… Больше результатов на GitHub нет - Не удалось загрузить с GitHub. Попробуйте снова. + Не удалось загрузить с GitHub. Попробуй снова. Совместимо с архитектурой Обновить до %1$s @@ -178,7 +178,7 @@ Разрешение на установку заблокировано политикой устройства Открыто во внешнем установщике Разрешение на установку недоступно - APK был успешно загружен, но это устройство не разрешает прямую установку. Хотите открыть его с помощью внешнего установщика? + APK был успешно загружен, но это устройство не разрешает прямую установку. Хочешь открыть его с помощью внешнего установщика? Открыть во внешнем установщике Использовать стороннее приложение для установки APK @@ -193,16 +193,16 @@ обновлено %1$s Превышен лимит запросов - Вы использовали все %1$d API-запросов. - Вы использовали все %1$d бесплатных API-запросов. + Ты использовал все %1$d API-запросов. + Ты использовал все %1$d бесплатных API-запросов. Сброс через %1$d мин - 💡 Войдите, чтобы получить 5 000 запросов в час вместо 60! + 💡 Войди, чтобы получить 5 000 запросов в час вместо 60! Войти ОК Закрыть Системный шрифт - Используйте шрифт вашего устройства для лучшей читаемости + Используй шрифт своего устройства для лучшей читаемости Светлая Тёмная @@ -223,13 +223,13 @@ Избранные репозитории Репозиторий добавлен в избранное Репозиторий не в избранном - Вы можете добавить репозиторий в избранное на GitHub - Вы можете убрать репозиторий из избранного на GitHub + Ты можешь добавить репозиторий в избранное на GitHub + Ты можешь убрать репозиторий из избранного на GitHub Требуется вход Нет избранных репозиториев - Войдите через GitHub, чтобы увидеть избранные репозитории - Отмечайте репозитории с установочными релизами на GitHub + Войди через GitHub, чтобы увидеть избранные репозитории + Отмечай репозитории с установочными релизами на GitHub Последняя синхронизация Только что %1$d мин назад @@ -334,10 +334,10 @@ Настройки прокси сохранены Не удалось сохранить настройки прокси Требуется хост прокси - Введите корректное имя хоста или IP-адрес + Введи корректное имя хоста или IP-адрес Недопустимый порт прокси Соединение в порядке (%1$d мс) - Не удалось разрешить хост. Проверьте адрес прокси. + Не удалось разрешить хост. Проверь адрес прокси. Не удалось подключиться к прокси-серверу. Время ожидания истекло. Требуется аутентификация прокси. @@ -348,23 +348,23 @@ Войти через GitHub - Откройте полный доступ. Управляйте приложениями, синхронизируйте настройки и просматривайте быстрее. + Открой полный доступ. Управляй приложениями, синхронизируй настройки и просматривай быстрее. Репозитории Войти - Ваши избранные репозитории на GitHub - Ваши избранные репозитории, сохранённые локально + Твои избранные репозитории на GitHub + Твои избранные репозитории, сохранённые локально Сессия истекла - Ваша сессия GitHub истекла или токен был отозван. Пожалуйста, войдите снова для продолжения использования авторизованных функций. - Вы можете продолжить просмотр как гость с ограниченным количеством API-запросов. + Твоя сессия GitHub истекла или токен был отозван. Пожалуйста, войди снова, чтобы продолжить пользоваться авторизованными функциями. + Ты можешь продолжить просмотр как гость с ограниченным количеством API-запросов. Войти снова Продолжить как гость - Это очистит вашу локальную сессию и кэшированные данные. Чтобы полностью отозвать доступ, перейдите в GitHub Settings > Applications. + Это очистит твою локальную сессию и кэшированные данные. Чтобы полностью отозвать доступ, перейди в GitHub Settings > Applications. Срок действия кода устройства истёк. - Пожалуйста, попробуйте войти снова для получения нового кода. - Проверьте подключение к интернету и попробуйте снова. - Вы отклонили запрос авторизации. Попробуйте снова, если это было непреднамеренно. + Пожалуйста, попробуй войти снова, чтобы получить новый код. + Проверь подключение к интернету и попробуй снова. + Ты отклонил запрос авторизации. Попробуй снова, если это было непреднамеренно. Я уже авторизовался Проверка… Превышен лимит — повтор через %1$d с @@ -379,7 +379,7 @@ Перевести на… Поиск языка - Ошибка перевода. Попробуйте ещё раз. + Ошибка перевода. Попробуй ещё раз. Открыть ссылку GitHub Обнаружена ссылка GitHub в буфере обмена @@ -411,8 +411,8 @@ Требуется разрешение Готов Предоставить разрешение - Установите Shizuku для тихой установки - Запустите Shizuku для тихой установки + Установи Shizuku для тихой установки + Запусти Shizuku для тихой установки Установка через Shizuku не удалась, используется стандартный установщик Автообновление приложений @@ -421,20 +421,20 @@ Интервал проверки обновлений Как часто проверять обновления приложения в фоне Фоновая проверка обновлений - Периодически проверяет обновления в фоне. Отключите для экономии батареи — всегда можно проверить вручную из экрана деталей любого приложения. + Периодически проверяет обновления в фоне. Отключи для экономии батареи — всегда можно проверить вручную из экрана деталей любого приложения. 12ч 24ч Разрешить обновления в фоне - Ваше устройство агрессивно завершает фоновые задачи. Исключите GitHub Store из оптимизации батареи, чтобы запланированные проверки обновлений и тихая установка работали надёжно. + Твоё устройство агрессивно завершает фоновые задачи. Исключи GitHub Store из оптимизации батареи, чтобы запланированные проверки обновлений и тихая установка работали надёжно. Открыть настройки Позже Отслеживать приложение Привязать приложение к репозиторию - Выберите установленное приложение для привязки к репозиторию GitHub + Выбери установленное приложение для привязки к репозиторию GitHub Поиск приложений… URL репозитория GitHub github.com/owner/repo @@ -444,17 +444,17 @@ Проверка ключа подписи… Несоответствие имени пакета: APK — %1$s, а выбранное приложение — %2$s Несоответствие ключа подписи: APK в этом репозитории подписан другим разработчиком - Выберите установщик - Выберите APK для проверки соответствия установленному приложению + Выбери установщик + Выбери APK, чтобы проверить соответствие установленному приложению Экспорт Импорт Канал бета по умолчанию - Вновь отслеживаемые приложения по умолчанию включают бета-сборки. Ранее отслеживаемые приложения сохраняют своё значение (переключите на экране Подробностей приложения). + Вновь отслеживаемые приложения по умолчанию включают бета-сборки. Ранее отслеживаемые приложения сохраняют своё значение (переключи на экране Подробностей приложения). Удалить приложение? - Вы уверены, что хотите удалить %1$s? Это действие нельзя отменить, данные приложения могут быть утеряны. - Неверный URL GitHub. Используйте формат: github.com/owner/repo + Точно хочешь удалить %1$s? Это действие нельзя отменить, данные приложения могут быть утеряны. + Неверный URL GitHub. Используй формат: github.com/owner/repo Репозиторий не найден: %1$s/%2$s - Превышен лимит запросов GitHub API. Попробуйте позже. + Превышен лимит запросов GitHub API. Попробуй позже. Не удалось привязать: %1$s Не удалось загрузить установленные приложения %1$s привязано к %2$s/%3$s @@ -470,7 +470,7 @@ Нет ресурсов, связанных с этим релизом Выбрать вариант ресурса Доступно несколько ресурсов - Для этого релиза доступно несколько устанавливаемых файлов. Просмотрите список и выберите подходящий для вашего устройства. + Для этого релиза доступно несколько устанавливаемых файлов. Просмотри список и выбери подходящий для своего устройства. Информация Выбрать язык Несоответствие пакета: APK — %1$s, но установленное приложение — %2$s. Обновление заблокировано. @@ -486,14 +486,14 @@ История просмотров очищена Просмотрено - Ваш репозиторий + Твой репозиторий Скрыть репозиторий Недавно просмотренные - Репозитории, которые вы посещали + Репозитории, которые ты посещал Удалить всё Удалить все загрузки? - Это навсегда удалит все APK и установщики (%1$s). Вы сможете скачать их снова в любое время. + Это навсегда удалит все APK и установщики (%1$s). Ты сможешь скачать их снова в любое время. Все загруженные пакеты удалены. Не удалось проверить @@ -519,14 +519,14 @@ Возврат к старым релизам Просматривать предыдущие релизы, пока не найдётся соответствующий фильтру. Необходимо для монорепо, где последний релиз принадлежит другому приложению. Расширенный фильтр - Настройте, как это приложение определяется в репозитории. Используйте эти настройки, если репо содержит несколько приложений. + Настрой, как это приложение определяется в репозитории. Используй эти настройки, если репо содержит несколько приложений. Открыть расширенный фильтр Сохранить Предпросмотр Обновить предпросмотр - Введите фильтр, чтобы увидеть подходящие ассеты. - В последних релизах нет подходящих ассетов. Включите возврат к старым релизам или измените regex. - Не удалось загрузить предпросмотр. Проверьте подключение и попробуйте снова. + Введи фильтр, чтобы увидеть подходящие ассеты. + В последних релизах нет подходящих ассетов. Включи возврат к старым релизам или измени regex. + Не удалось загрузить предпросмотр. Проверь подключение и попробуй снова. Совпадение в %1$s · %2$d ассет Совпадение в %1$s · %2$d ассета @@ -536,22 +536,22 @@ Предпочтительный вариант - Выберите, какой вариант APK будет устанавливаться при обновлениях. Выбор запоминается между релизами. + Выбери, какой вариант APK будет устанавливаться при обновлениях. Выбор запоминается между релизами. В последнем релизе нет устанавливаемых ассетов. - В этом релизе нет вариантов с тегом версии, которые можно закрепить. Автоматический селектор продолжит выбирать лучший ассет для вашего устройства. - Не удалось загрузить варианты. Проверьте подключение и попробуйте снова. - Не удалось сохранить выбор. Попробуйте снова. - Вариант изменился — выберите снова + В этом релизе нет вариантов с тегом версии, которые можно закрепить. Автоматический селектор продолжит выбирать лучший ассет для твоего устройства. + Не удалось загрузить варианты. Проверь подключение и попробуй снова. + Не удалось сохранить выбор. Попробуй снова. + Вариант изменился — выбери снова Было: %1$s Автоматически - Пусть GitHub Store сам выберет лучший вариант для вашего устройства + Пусть GitHub Store сам выберет лучший вариант для твоего устройства Примечание Совет Важно Предупреждение Осторожно Закреплено: %1$s - Вариант изменился — нажмите Обновить, чтобы выбрать снова + Вариант изменился — нажми Обновить, чтобы выбрать снова Вариант: %1$s Открепить Закреплено @@ -565,13 +565,13 @@ Готово к установке Установить (готово) - Выберите сервис для перевода README. + Выбери сервис для перевода README. Сервис перевода Google работает глобально без настройки. Youdao работает из материкового Китая, но требует учётных данных API с портала разработчиков Youdao. Google Youdao Сервис перевода обновлён - Зарегистрируйтесь на ai.youdao.com, чтобы получить App Key и App Secret. Бесплатного тарифа достаточно для обычного использования. + Зарегистрируйся на ai.youdao.com, чтобы получить App Key и App Secret. Бесплатного тарифа достаточно для обычного использования. App Key App Secret Сохранить учётные данные @@ -579,12 +579,12 @@ Не удалось загрузить релизы - Проверьте подключение и попробуйте снова. + Проверь подключение и попробуй снова. Релизы ещё не опубликованы Загрузка релизов… - Проблемы со входом? Используйте Personal Access Token + Проблемы со входом? Используй Personal Access Token Использовать код устройства F-Droid / Obtainium Установлено вручную @@ -601,17 +601,17 @@ Поиск Вручную Вход с Personal Access Token - Создайте Personal Access Token (classic или fine-grained) на устройстве, где доступен GitHub, и вставьте его ниже. Требуется доступ на чтение к репозиториям и вашему профилю. + Создай Personal Access Token (classic или fine-grained) на устройстве, где доступен GitHub, и вставь его ниже. Требуется доступ на чтение к репозиториям и твоему профилю. Открыть настройки токенов GitHub Personal Access Token ghp_… или github_pat_… Войти Отмена - Вставьте токен. + Вставь токен. Это не похоже на токен GitHub. Токены начинаются с ghp_ или github_pat_. Этот токен недействителен или отозван. У этого токена нет необходимых прав. - Не удалось сохранить токен. Попробуйте снова. + Не удалось сохранить токен. Попробуй снова. Включить бета-версии Только стабильные - Выберите канал релизов - Нажмите, чтобы переключаться между стабильными релизами и бета-сборками этого приложения. + Выбери канал релизов + Нажми, чтобы переключаться между стабильными релизами и бета-сборками этого приложения. Понятно Перейти на стабильную %1$s Нет стабильного релиза уже %1$d месяцев @@ -656,20 +656,20 @@ Другие совпадения Скрыть совпадения Установлено через %1$s - Нажмите, чтобы найти репозиторий + Нажми, чтобы найти репозиторий Развернуть для просмотра других совпадений Свернуть карточку Поиск в GitHub Нет совпадений Ошибка поиска - Вставьте URL github.com или выполните поиск… + Вставь URL github.com или выполни поиск… Отслеживается %1$d приложение. Отслеживается %1$d приложения. Отслеживается %1$d приложений. Отслеживается %1$d приложения. - Пропущено %1$d — повторите сканирование в Настройках. + Пропущено %1$d — повтори сканирование в Настройках. Посмотреть все Всё связано.\nНечего связывать. Готово @@ -678,8 +678,8 @@ Предоставить разрешение ОК Добавить приложение из другого магазина - Найдите ваши приложения GitHub - Мы можем просканировать установленные приложения и сопоставить их с релизами GitHub, чтобы обновления и обнаружение работали автоматически.\n\nДля этого нам нужно видеть, какие приложения у вас установлены. Без разрешения мы можем видеть только около 5 приложений; с ним — все.\n\nМы никогда не отправляем список ваших приложений без вашего согласия. Сопоставление выполняется на вашем устройстве. Необязательный серверный поиск передаёт только имя пакета и метку приложения, а при наличии — отпечаток подписи, источник установки и любую подсказку о репозитории GitHub, указанную в манифесте приложения; полный список установленных приложений никогда не передаётся. + Найди свои приложения GitHub + Мы можем просканировать установленные приложения и сопоставить их с релизами GitHub, чтобы обновления и обнаружение работали автоматически.\n\nДля этого нам нужно видеть, какие приложения у тебя установлены. Без разрешения мы можем видеть только около 5 приложений; с ним — все.\n\nМы никогда не отправляем список твоих приложений без твоего согласия. Сопоставление выполняется на твоём устройстве. Необязательный серверный поиск передаёт только имя пакета и метку приложения, а при наличии — отпечаток подписи, источник установки и любую подсказку о репозитории GitHub, указанную в манифесте приложения; полный список установленных приложений никогда не передаётся. Продолжить Не сейчас @@ -688,13 +688,13 @@ Найдено %1$d приложений из GitHub Найдено %1$d приложения из GitHub - Просмотрите их, чтобы отслеживать обновления здесь. + Просмотри их, чтобы отслеживать обновления здесь. Просмотреть Закрыть Достоверность совпадения: %1$d процентов %1$d - Не удалось связать это приложение — попробуйте ещё раз. - Не удалось подключиться к GitHub. Попробуйте позже. + Не удалось связать это приложение — попробуй ещё раз. + Не удалось подключиться к GitHub. Попробуй позже. Ошибка сканирования Obtainium F-Droid @@ -705,26 +705,26 @@ %1$s связано. %1$s пропущено. Отменить - Не удалось отменить — попробуйте ещё раз. + Не удалось отменить — попробуй ещё раз. %1$d приложение для просмотра %1$d приложения для просмотра %1$d приложений для просмотра %1$d приложения для просмотра - Не видите своё приложение? Добавить вручную + Не видишь своё приложение? Добавь вручную %1$d приложение связано автоматически %1$d приложения связано автоматически %1$d приложений связано автоматически %1$d приложения связано автоматически - Мы распознали их с высокой точностью. Вы можете отменить отдельные совпадения в деталях каждого приложения или отменить все сейчас. + Мы распознали их с высокой точностью. Ты можешь отменить отдельные совпадения в деталях каждого приложения или отменить все сейчас. - Ещё %1$d требует вашего внимания. - Ещё %1$d требуют вашего внимания. - Ещё %1$d требуют вашего внимания. - Ещё %1$d требуют вашего внимания. + Ещё %1$d требует твоего внимания. + Ещё %1$d требуют твоего внимания. + Ещё %1$d требуют твоего внимания. + Ещё %1$d требуют твоего внимания. Продолжить Отменить все @@ -739,20 +739,20 @@ Мы перестанем отслеживать %1$s как установленное из этого репозитория. Приложение останется на устройстве — удаляется только связь. Отвязать Отвязано. При следующем сканировании мы предложим совпадение. - Не удалось отвязать — попробуйте ещё раз. + Не удалось отвязать — попробуй ещё раз. Связан с %1$s/%2$s - Привязано вручную — проверено вами. Отвяжите, чтобы пересканировать. + Привязано вручную — проверено тобой. Отвяжи, чтобы пересканировать. Обновить Обновить (%1$dс) Дополнительно - Только что обновлено — повторите через %1$d с. - Сервис обновления занят — повторите через %1$d с. + Только что обновлено — повтори через %1$d с. + Сервис обновления занят — повтори через %1$d с. Этот репозиторий заархивирован на GitHub. Этот репозиторий больше не существует на GitHub. - Не удалось обновить. Повторите попытку позже. - Ошибка обновления. Повторите попытку. + Не удалось обновить. Повтори попытку позже. + Ошибка обновления. Повтори попытку. Свои форджи Свои форджи - Введите имя хоста Forgejo или Gitea (например git.example.com). GHS будет принимать URL с этих хостов в форме ручной привязки. + Введи имя хоста Forgejo или Gitea (например git.example.com). GHS будет принимать URL с этих хостов в форме ручной привязки. Добавить Пока нет своих форджей. Готово @@ -820,7 +820,7 @@ Превышено время ожидания (5s) Не удалось разрешить имя хоста Ошибка: %1$s - Все зеркала недоступны? За 5 минут можно развернуть своё — смотрите документацию. + Все зеркала недоступны? За 5 минут можно развернуть своё — смотри документацию. %1$s больше недоступно, выполнен переход на прямой доступ через GitHub. @@ -845,9 +845,9 @@ Объявления Подробнее Проверить APK - Разрешения, подпись, компоненты — посмотрите, что именно объявляет APK. + Разрешения, подпись, компоненты — посмотри, что именно объявляет APK. Понятно - Загляните внутрь перед установкой + Загляни внутрь перед установкой Совместимость Компоненты Отладочная сборка — подозрительно для релиза. @@ -892,28 +892,28 @@ Отбросить отложенный установщик для %1$s и удалить строку из списка приложений? Загруженный APK будет удалён. Отбросить ожидающую установку? Предоставить разрешение - Установите Dhizuku и активируйте его как Device Owner для тихих установок - Активируйте Dhizuku как Device Owner для тихих установок + Установи Dhizuku и активируй его как Device Owner для тихих установок + Активируй Dhizuku как Device Owner для тихих установок Root Тихая установка через Magisk, KernelSU или APatch root Предоставлен Разрешить root Нет root Разрешить root-доступ - Root недоступен на этом устройстве. Сначала установите Magisk, KernelSU или APatch. + Root недоступен на этом устройстве. Сначала установи Magisk, KernelSU или APatch. Dhizuku не установлен Dhizuku не активен Требуется разрешение Готов Отбросить Назад - Не удалось загрузить этот репо. Используйте кнопку «Повторить» ниже. + Не удалось загрузить этот репо. Нажми «Повторить» ниже. Репозиторий мог стать приватным, быть переименован или удалён. - Проверьте подключение и попробуйте снова. - GitHub или наш бэкенд достиг квоты. Попробуйте чуть позже. + Проверь подключение и попробуй снова. + GitHub или наш бэкенд достиг квоты. Попробуй чуть позже. Что-то пошло не так Репозиторий не найден - Вы офлайн + Ты офлайн Достигнут лимит запросов Экспорт для Obtainium от %1$s @@ -933,7 +933,7 @@ Это не похоже на экспорт GitHub Store или Obtainium. Вот начало файла: Не удалось прочитать этот файл Применить - Используйте корректное имя Android-пакета (например, com.example.installer) + Используй корректное имя Android-пакета (например, com.example.installer) Имя пакета установщика Приложения видят, какой установщик поместил их на устройство. По умолчанию это системная оболочка. Некоторые приложения отслеживают смену установщика и могут отказаться запускаться или не пройти проверки безопасности (например, Play Integrity, банковские приложения). @@ -946,19 +946,19 @@ Dhizuku Тихая установка через Device Owner — обходит подсказки установки OEM Лимит достигнут — повтор через %1$d с. - Войдите в GitHub для большей квоты. - Релизы временно недоступны. Попробуйте чуть позже. + Войди в GitHub для большей квоты. + Релизы временно недоступны. Попробуй чуть позже. Отслеживается APK - Нет избранных репо. Отметьте репо звездой на GitHub, чтобы увидеть их здесь. + Нет избранных репо. Отметь репо звездой на GitHub, чтобы увидеть их здесь. Показывать репо без релизов APK %1$d избранных · %2$d с APK · %3$d уже отслеживается Ничего не найдено. Проверка %1$d / %2$d… - Достигнут лимит GitHub. Возобновите, чтобы продолжить проверку. + Достигнут лимит GitHub. Возобнови, чтобы продолжить проверку. Возобновить Поиск по владельцу / репо / описанию - Войдите в GitHub, чтобы увидеть избранные репо. + Войди в GitHub, чтобы увидеть избранные репо. А → Я Недавно отмеченные Больше звёзд @@ -979,15 +979,15 @@ Пропустить эту версию Не пропускать - Версия %1$s пропущена — вы получите уведомление, когда выйдет более новая + Версия %1$s пропущена — ты получишь уведомление, когда выйдет более новая Уведомление об обновлении снова включено Не удалось обновить настройку Не удалось восстановить уведомление об обновлении Пропущенные обновления - Версии, которые вы решили игнорировать + Версии, которые ты решил игнорировать Пропущенные обновления Нет пропущенных версий - Когда вы пропускаете обновление на экране приложений, запись появляется здесь, чтобы её можно было отменить. + Когда ты пропускаешь обновление на экране приложений, запись появляется здесь, чтобы её можно было отменить. Пропущена %1$s Установлена: %1$s Не пропускать @@ -995,14 +995,13 @@ Не удалось обновить настройку Скрытые репозитории Управление репозиториями, скрытыми с Главной и Поиска. - %1$d скрыто Нет скрытых репозиториев - Удерживайте карточку репозитория на Главной или в Поиске, чтобы скрыть её из обзора. + Удерживай карточку репозитория на Главной или в Поиске, чтобы скрыть её из обзора. Показать Показать все %1$s снова виден Все репозитории снова видны - Не удалось показать. Повторите попытку. + Не удалось показать. Повтори попытку. Открыть на GitHub Отметить как просмотренный Снять отметку @@ -1057,7 +1056,7 @@ Мы читаем каждый отзыв. Получить #%1$d - Из ваших избранных + Из твоих избранных Свежие релизы Самые популярные В тренде @@ -1273,4 +1272,10 @@ Занято: %1$s Этот экран — часть продолжающегося редизайна. Сами настройки работают — их просто переносят сюда со старого экрана. Появится в следующем обновлении + + %1$d репозиторий скрыт + %1$d репозитория скрыто + %1$d репозиториев скрыто + %1$d репозитория скрыто + From c9f1c80e668152289f9ddf211904acfccad3f572 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Sun, 24 May 2026 16:38:44 +0500 Subject: [PATCH 124/172] i18n: dedupe hidden_repositories_count to plurals across all locales --- .../commonMain/composeResources/values-ar/strings-ar.xml | 9 ++++++++- .../commonMain/composeResources/values-bn/strings-bn.xml | 1 - .../commonMain/composeResources/values-es/strings-es.xml | 5 ++++- .../commonMain/composeResources/values-fr/strings-fr.xml | 5 ++++- .../commonMain/composeResources/values-hi/strings-hi.xml | 1 - .../commonMain/composeResources/values-it/strings-it.xml | 5 ++++- .../commonMain/composeResources/values-ja/strings-ja.xml | 4 +++- .../commonMain/composeResources/values-ko/strings-ko.xml | 4 +++- .../commonMain/composeResources/values-pl/strings-pl.xml | 7 ++++++- .../commonMain/composeResources/values-tr/strings-tr.xml | 1 - .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 4 +++- .../src/commonMain/composeResources/values/strings.xml | 1 - .../tweaks/presentation/hidden/HiddenRepositoriesRoot.kt | 7 ++++++- 13 files changed, 41 insertions(+), 13 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 2d2110be0..d4bfaa6ad 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1017,7 +1017,6 @@ تعذّر تحديث التفضيل المستودعات المخفية إدارة المستودعات التي أخفيتها من الرئيسية والبحث. - %1$d مخفي لا توجد مستودعات مخفية اضغط مطولاً على بطاقة مستودع في الرئيسية أو البحث لإخفائه من الاستكشاف. إظهار @@ -1301,4 +1300,12 @@ يستخدم: %1$s هذه الشاشة جزء من التصميم الجديد قيد التطوير. الإعدادات لا تزال تعمل — يجري نقلها إلى هنا من التخطيط القديم. قادم في تحديث لاحق + + لا مستودعات مخفية + مستودع مخفي واحد + مستودعان مخفيان + %1$d مستودعات مخفية + %1$d مستودعًا مخفيًا + %1$d مستودع مخفي + diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index ac4af39ce..e710f5d01 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -995,7 +995,6 @@ পছন্দ আপডেট করা যায়নি লুকানো রিপোজিটরি হোম ও সার্চ থেকে লুকানো রিপোজিটরি পরিচালনা করুন। - %1$d লুকানো কোনো লুকানো রিপোজিটরি নেই হোম বা সার্চে রিপোজিটরি কার্ডে দীর্ঘক্ষণ চাপ দিয়ে আবিষ্কার থেকে লুকান। দেখান diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index e1b111f02..1eca6e504 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -974,7 +974,6 @@ No se pudo actualizar la preferencia Repositorios ocultos Gestiona los repositorios que ocultaste de Inicio y Búsqueda. - %1$d oculto(s) Sin repositorios ocultos Mantén pulsada una tarjeta de repositorio en Inicio o Búsqueda para ocultarla. Mostrar @@ -1246,4 +1245,8 @@ En uso: %1$s Esta pantalla forma parte del rediseño en curso. Los ajustes siguen funcionando — solo los estamos moviendo aquí desde el diseño anterior. Llegará en una próxima versión + + %1$d repo oculto + %1$d repos ocultos + diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index e1831306f..b36415c76 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -975,7 +975,6 @@ Impossible de mettre à jour la préférence Dépôts masqués Gérez les dépôts masqués depuis Accueil et Recherche. - %1$d masqué(s) Aucun dépôt masqué Appuyez longuement sur une carte de dépôt dans Accueil ou Recherche pour la masquer. Afficher @@ -1246,4 +1245,8 @@ Utilisation : %1$s Cet écran fait partie de la refonte en cours. Les paramètres fonctionnent toujours — ils sont simplement en cours de migration depuis l\'ancienne disposition. Bientôt disponible + + %1$d dépôt masqué + %1$d dépôts masqués + diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 297cae707..9fa2a6eac 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1006,7 +1006,6 @@ प्राथमिकता अपडेट नहीं हो सकी छिपे हुए रिपॉजिटरी होम और सर्च से छिपाए गए रिपॉजिटरी प्रबंधित करें। - %1$d छिपे हुए कोई छिपा रिपॉजिटरी नहीं होम या सर्च में रेपो कार्ड पर लंबा दबाकर इसे डिस्कवरी से छिपाएं। दिखाएं diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 71a5ad282..c71be6ecc 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1008,7 +1008,6 @@ Impossibile aggiornare la preferenza Repository nascosti Gestisci i repository nascosti da Home e Ricerca. - %1$d nascosti Nessun repository nascosto Tieni premuta una scheda repository in Home o Ricerca per nasconderla. Mostra @@ -1280,4 +1279,8 @@ In uso: %1$s Questa schermata fa parte del redesign in corso. Le impostazioni funzionano ancora — vengono solo spostate qui dal vecchio layout. In arrivo + + %1$d repo nascosto + %1$d repo nascosti + diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 80fedb0f7..0b0bfdb6b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -969,7 +969,6 @@ 設定を更新できませんでした 非表示のリポジトリ ホームと検索で非表示にしたリポジトリを管理。 - %1$d 件非表示 非表示のリポジトリはありません ホームや検索でリポジトリカードを長押しすると検索結果から非表示にできます。 表示する @@ -1237,4 +1236,7 @@ 使用中: %1$s この画面は進行中のリデザインの一部です。設定は引き続き動作します — 旧来のレイアウトからここへ移動している途中です。 今後のアップデートで実装予定 + + %1$d 件非表示 + diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 6a90bc954..4001a04f2 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -998,7 +998,6 @@ 설정을 업데이트할 수 없습니다 숨긴 저장소 홈과 검색에서 숨긴 저장소를 관리합니다. - %1$d개 숨김 숨긴 저장소 없음 홈 또는 검색에서 저장소 카드를 길게 눌러 탐색에서 숨길 수 있습니다. 표시 @@ -1267,4 +1266,7 @@ 사용 중: %1$s 이 화면은 진행 중인 디자인 개편의 일부입니다. 설정은 정상 동작하며 — 이전 레이아웃에서 이곳으로 옮겨지는 중일 뿐입니다. 후속 업데이트 예정 + + %1$d개 숨김 + diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 1f9efa336..555a87f89 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -995,7 +995,6 @@ Nie udało się zmienić preferencji Ukryte repozytoria Zarządzaj repozytoriami ukrytymi na Ekranie głównym i w Wyszukiwarce. - %1$d ukryte Brak ukrytych repozytoriów Przytrzymaj kartę repozytorium na Ekranie głównym lub w Wyszukiwarce, aby ukryć ją z odkrywania. Pokaż @@ -1273,4 +1272,10 @@ Zajmuje: %1$s Ten ekran jest częścią trwającego przeprojektowania. Ustawienia nadal działają — są tylko przenoszone tutaj ze starego układu. Wkrótce w kolejnej aktualizacji + + %1$d repo + %1$d repo + %1$d repo + %1$d repo + diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 662e47216..40f9668a5 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1005,7 +1005,6 @@ Tercih güncellenemedi Gizli depolar Ana ekran ve Arama'da gizlediğin depoları yönet. - %1$d gizli Gizli depo yok Ana ekran veya Arama'da bir depo kartına basılı tutarak keşiften gizleyin. Göster diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index af263bc98..32b7e89f6 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -971,7 +971,6 @@ 无法更新偏好设置 已隐藏的仓库 管理从首页和搜索中隐藏的仓库。 - 已隐藏 %1$d 个 没有隐藏的仓库 在首页或搜索中长按仓库卡片即可从发现中隐藏。 取消隐藏 @@ -1240,4 +1239,7 @@ 占用:%1$s 此页面属于正在进行的重新设计。设置依然有效 — 只是正从旧布局迁移到这里。 后续更新中 + + 已隐藏 %1$d 个 + diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 83dc0c5b8..177ccb39d 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -683,7 +683,6 @@ For transfer Hidden repositories Manage repositories you hid from Home and Search. - %1$d hidden No hidden repositories Long-press a repository card on Home or Search to hide it from discovery. Unhide diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt index 3f0377c1b..105cbc8f4 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt @@ -49,6 +49,7 @@ import zed.rainxch.core.presentation.components.GitHubStoreImage import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res +import org.jetbrains.compose.resources.pluralStringResource import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_count import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_empty_description import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_empty_title @@ -107,7 +108,11 @@ fun HiddenRepositoriesRoot( ) if (state.items.isNotEmpty()) { Text( - text = stringResource(Res.string.hidden_repositories_count, state.items.size), + text = pluralStringResource( + Res.plurals.hidden_repositories_count, + state.items.size, + state.items.size, + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) From 94e3835a83dad57d3088282cd1d653b0ec241f4c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 06:35:20 +0500 Subject: [PATCH 125/172] feat(tweaks): community socials showcase in App info --- .../composeResources/values/strings.xml | 6 + .../presentation/appinfo/TweaksAppInfoRoot.kt | 221 +++++++++++++++++- 2 files changed, 226 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 177ccb39d..7b43a0e96 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1300,6 +1300,12 @@ GitHub Store Cross-platform app store for GitHub, Codeberg, and Forgejo releases. + Connect + Join the community + 6 places + Business inquiries + Partnerships, press, integrations. + Contact What\'s new Past release notes. Open source licenses diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index 5f7cb3528..dbbb30e53 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -13,12 +13,19 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.AlternateEmail import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.Forum +import androidx.compose.material.icons.outlined.Language import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Store +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState @@ -43,7 +50,17 @@ import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.tweaks.presentation.components.TweaksAccents import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant +import zed.rainxch.tweaks.presentation.components.SectionHeader import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_app_name +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_business_cta +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_business_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_business_title +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_section +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_subtitle +import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_title import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_licenses_subtitle import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_licenses_title import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_privacy_policy_subtitle @@ -61,6 +78,14 @@ import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold private const val PRIVACY_POLICY_URL = "https://github-store.org/privacy" private const val SOURCE_CODE_URL = "https://github.com/OpenHub-Store/GitHub-Store" +private const val TELEGRAM_URL = "https://t.me/githubstore" +private const val DISCORD_URL = "https://discord.gg/githubstore" +private const val MASTODON_URL = "https://fosstodon.org/@githubstore" +private const val REDDIT_URL = "https://reddit.com/r/githubstore" +private const val GITHUB_ORG_URL = "https://github.com/OpenHub-Store" +private const val WEBSITE_URL = "https://github-store.org" +private const val BUSINESS_EMAIL = "mailto:contact@github-store.org" + @Composable fun TweaksAppInfoRoot( onNavigateBack: () -> Unit, @@ -83,7 +108,25 @@ fun TweaksAppInfoRoot( ) { item(key = "app_identity") { AppIdentityCard(versionName = state.versionName) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) + } + + item(key = "community_section_header") { + SectionHeader(text = stringResource(Res.string.tweaks_app_info_community_section)) + Spacer(Modifier.height(8.dp)) + } + + item(key = "community_card") { + CommunityCard( + onTelegram = { runCatching { uriHandler.openUri(TELEGRAM_URL) } }, + onDiscord = { runCatching { uriHandler.openUri(DISCORD_URL) } }, + onMastodon = { runCatching { uriHandler.openUri(MASTODON_URL) } }, + onReddit = { runCatching { uriHandler.openUri(REDDIT_URL) } }, + onGithub = { runCatching { uriHandler.openUri(GITHUB_ORG_URL) } }, + onWebsite = { runCatching { uriHandler.openUri(WEBSITE_URL) } }, + onBusiness = { runCatching { uriHandler.openUri(BUSINESS_EMAIL) } }, + ) + Spacer(Modifier.height(20.dp)) } item(key = "action_whats_new") { @@ -194,6 +237,182 @@ private fun AppIdentityCard(versionName: String) { } } +@Composable +private fun CommunityCard( + onTelegram: () -> Unit, + onDiscord: () -> Unit, + onMastodon: () -> Unit, + onReddit: () -> Unit, + onGithub: () -> Unit, + onWebsite: () -> Unit, + onBusiness: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(Res.string.tweaks_app_info_community_title), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Text( + text = stringResource(Res.string.tweaks_app_info_community_subtitle), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(14.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + SocialTile( + label = "Telegram", + icon = Icons.AutoMirrored.Filled.Send, + accent = TweaksAccents.Sky, + onClick = onTelegram, + modifier = Modifier.weight(1f), + ) + SocialTile( + label = "Discord", + icon = Icons.Outlined.Forum, + accent = TweaksAccents.Periwinkle, + onClick = onDiscord, + modifier = Modifier.weight(1f), + ) + SocialTile( + label = "Mastodon", + icon = Icons.Outlined.AlternateEmail, + accent = TweaksAccents.Lavender, + onClick = onMastodon, + modifier = Modifier.weight(1f), + ) + } + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + SocialTile( + label = "Reddit", + icon = Icons.Outlined.Public, + accent = TweaksAccents.Peach, + onClick = onReddit, + modifier = Modifier.weight(1f), + ) + SocialTile( + label = "GitHub", + icon = Icons.Outlined.Code, + accent = TweaksAccents.Tan, + onClick = onGithub, + modifier = Modifier.weight(1f), + ) + SocialTile( + label = "Website", + icon = Icons.Outlined.Language, + accent = TweaksAccents.Sage, + onClick = onWebsite, + modifier = Modifier.weight(1f), + ) + } + + Spacer(Modifier.height(16.dp)) + HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)) + Spacer(Modifier.height(14.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(Res.string.tweaks_app_info_community_business_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = stringResource(Res.string.tweaks_app_info_community_business_subtitle), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + GhsButton( + onClick = onBusiness, + label = stringResource(Res.string.tweaks_app_info_community_business_cta), + variant = GhsButtonVariant.Outline, + size = GhsButtonSize.Sm, + trailingIcon = Icons.AutoMirrored.Filled.ArrowForward, + ) + } + } + } +} + +@Composable +private fun SocialTile( + label: String, + icon: ImageVector, + accent: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier + .clip(Radii.row) + .clickable(onClick = onClick), + shape = Radii.row, + color = accent.copy(alpha = 0.14f), + border = BorderStroke(1.dp, accent.copy(alpha = 0.35f)), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp, horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Box( + modifier = Modifier + .size(36.dp) + .clip(Radii.chip) + .background(accent.copy(alpha = 0.22f)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = accent, + modifier = Modifier.size(20.dp), + ) + } + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + @Composable private fun ActionRow( icon: ImageVector, From 56c84b73e2672c6dd8e7452080e414a7f232995a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 06:38:01 +0500 Subject: [PATCH 126/172] i18n: translate socials strings into 12 locales --- .../commonMain/composeResources/values-ar/strings-ar.xml | 6 ++++++ .../commonMain/composeResources/values-bn/strings-bn.xml | 6 ++++++ .../commonMain/composeResources/values-es/strings-es.xml | 6 ++++++ .../commonMain/composeResources/values-fr/strings-fr.xml | 6 ++++++ .../commonMain/composeResources/values-hi/strings-hi.xml | 6 ++++++ .../commonMain/composeResources/values-it/strings-it.xml | 6 ++++++ .../commonMain/composeResources/values-ja/strings-ja.xml | 6 ++++++ .../commonMain/composeResources/values-ko/strings-ko.xml | 6 ++++++ .../commonMain/composeResources/values-pl/strings-pl.xml | 6 ++++++ .../commonMain/composeResources/values-ru/strings-ru.xml | 6 ++++++ .../commonMain/composeResources/values-tr/strings-tr.xml | 6 ++++++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 6 ++++++ 12 files changed, 72 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index d4bfaa6ad..51d26fc8f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1308,4 +1308,10 @@ %1$d مستودعًا مخفيًا %1$d مستودع مخفي + تواصل + انضم إلى المجتمع + 6 أماكن + استفسارات الأعمال + الشراكات، الصحافة، التكاملات. + تواصل diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index e710f5d01..2f9fa0de8 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1270,4 +1270,10 @@ ব্যবহার: %1$s এই স্ক্রিনটি চলমান নতুন ডিজাইনের অংশ। সেটিংসগুলো এখনো কাজ করে — শুধু পুরোনো লেআউট থেকে এখানে সরানো হচ্ছে। পরবর্তী আপডেটে আসছে + যোগাযোগ + কমিউনিটিতে যোগ দিন + ৬টি স্থান + ব্যবসায়িক অনুসন্ধান + অংশীদারি, প্রেস, ইন্টিগ্রেশন। + যোগাযোগ diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 1eca6e504..e0491bd2f 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1249,4 +1249,10 @@ %1$d repo oculto %1$d repos ocultos + Conectar + Únete a la comunidad + 6 lugares + Consultas comerciales + Alianzas, prensa, integraciones. + Contactar diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index b36415c76..fcb47d957 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1249,4 +1249,10 @@ %1$d dépôt masqué %1$d dépôts masqués + Se connecter + Rejoignez la communauté + 6 endroits + Demandes professionnelles + Partenariats, presse, intégrations. + Contacter diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 9fa2a6eac..1f31635ee 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1285,4 +1285,10 @@ %1$d ऐप %1$d ऐप + जुड़ें + समुदाय में शामिल हों + 6 जगह + व्यावसायिक पूछताछ + साझेदारी, प्रेस, इंटीग्रेशन। + संपर्क करें diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index c71be6ecc..d5ac2c63f 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1283,4 +1283,10 @@ %1$d repo nascosto %1$d repo nascosti + Connettiti + Unisciti alla community + 6 luoghi + Richieste commerciali + Partnership, stampa, integrazioni. + Contattaci diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 0b0bfdb6b..3668e8b5b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1239,4 +1239,10 @@ %1$d 件非表示 + つながる + コミュニティに参加 + 6 か所 + ビジネスのお問い合わせ + パートナーシップ、プレス、連携。 + お問い合わせ diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 4001a04f2..f6d10e3aa 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1269,4 +1269,10 @@ %1$d개 숨김 + 연결 + 커뮤니티 참여 + 6곳 + 비즈니스 문의 + 파트너십, 보도, 연동. + 문의 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 555a87f89..2f89c88af 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1278,4 +1278,10 @@ %1$d repo %1$d repo + Połącz się + Dołącz do społeczności + 6 miejsc + Sprawy biznesowe + Współprace, prasa, integracje. + Kontakt diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index ea6c32212..1ebd926e9 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1278,4 +1278,10 @@ %1$d репозиториев скрыто %1$d репозитория скрыто + Связь + Присоединяйся к сообществу + 6 мест + Деловые запросы + Партнёрства, пресса, интеграции. + Написать diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 40f9668a5..c759e17c1 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1280,4 +1280,10 @@ Kullanılıyor: %1$s Bu ekran sürmekte olan yeniden tasarımın bir parçası. Ayarlar hâlâ çalışıyor — sadece eski düzenden buraya taşınıyor. Yakında bir sonraki sürümde + Bağlan + Topluluğa katıl + 6 yer + Ticari sorular + Ortaklıklar, basın, entegrasyonlar. + İletişim diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 32b7e89f6..98b37b1a8 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1242,4 +1242,10 @@ 已隐藏 %1$d 个 + 连接 + 加入社区 + 6 个平台 + 商务咨询 + 合作、媒体、集成。 + 联系 From 67d2683750bf005322b1c79be7d1698bfdc6fd23 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 06:46:50 +0500 Subject: [PATCH 127/172] tweaks(licenses): load library list from JSON asset --- .../composeResources/files/licenses.json | 28 +++++ feature/tweaks/presentation/build.gradle.kts | 1 + .../presentation/licenses/LicensesRoot.kt | 104 ++++++++++++------ 3 files changed, 97 insertions(+), 36 deletions(-) create mode 100644 core/presentation/src/commonMain/composeResources/files/licenses.json diff --git a/core/presentation/src/commonMain/composeResources/files/licenses.json b/core/presentation/src/commonMain/composeResources/files/licenses.json new file mode 100644 index 000000000..1dc3e378c --- /dev/null +++ b/core/presentation/src/commonMain/composeResources/files/licenses.json @@ -0,0 +1,28 @@ +{ + "generatedAt": "2026-05-25", + "note": "Maintainer-curated. To regenerate, run ./gradlew :composeApp:printRuntimeDependencies and reconcile against this list.", + "libraries": [ + {"name": "Kotlin", "license": "Apache-2.0", "url": "https://github.com/JetBrains/kotlin"}, + {"name": "Compose Multiplatform", "license": "Apache-2.0", "url": "https://github.com/JetBrains/compose-multiplatform"}, + {"name": "Jetpack Compose", "license": "Apache-2.0", "url": "https://developer.android.com/jetpack/compose"}, + {"name": "Ktor", "license": "Apache-2.0", "url": "https://github.com/ktorio/ktor"}, + {"name": "Room", "license": "Apache-2.0", "url": "https://developer.android.com/jetpack/androidx/releases/room"}, + {"name": "Koin", "license": "Apache-2.0", "url": "https://github.com/InsertKoinIO/koin"}, + {"name": "kotlinx.serialization", "license": "Apache-2.0", "url": "https://github.com/Kotlin/kotlinx.serialization"}, + {"name": "kotlinx.coroutines", "license": "Apache-2.0", "url": "https://github.com/Kotlin/kotlinx.coroutines"}, + {"name": "kotlinx.datetime", "license": "Apache-2.0", "url": "https://github.com/Kotlin/kotlinx-datetime"}, + {"name": "kotlinx.collections.immutable", "license": "Apache-2.0", "url": "https://github.com/Kotlin/kotlinx.collections.immutable"}, + {"name": "DataStore", "license": "Apache-2.0", "url": "https://developer.android.com/jetpack/androidx/releases/datastore"}, + {"name": "Landscapist", "license": "Apache-2.0", "url": "https://github.com/skydoves/landscapist"}, + {"name": "Coil", "license": "Apache-2.0", "url": "https://github.com/coil-kt/coil"}, + {"name": "Kermit", "license": "Apache-2.0", "url": "https://github.com/touchlab/Kermit"}, + {"name": "MOKO Permissions", "license": "Apache-2.0", "url": "https://github.com/icerockdev/moko-permissions"}, + {"name": "Navigation Compose", "license": "Apache-2.0", "url": "https://developer.android.com/jetpack/androidx/releases/navigation"}, + {"name": "multiplatform-markdown-renderer", "license": "Apache-2.0", "url": "https://github.com/mikepenz/multiplatform-markdown-renderer"}, + {"name": "Shizuku", "license": "Apache-2.0", "url": "https://github.com/RikkaApps/Shizuku"}, + {"name": "WorkManager", "license": "Apache-2.0", "url": "https://developer.android.com/jetpack/androidx/releases/work"}, + {"name": "KSafe", "license": "Apache-2.0", "url": "https://github.com/Anifantakis/KSafe"}, + {"name": "Material Icons Extended", "license": "Apache-2.0", "url": "https://fonts.google.com/icons"}, + {"name": "Geist (Vercel)", "license": "OFL-1.1", "url": "https://github.com/vercel/geist-font"} + ] +} diff --git a/feature/tweaks/presentation/build.gradle.kts b/feature/tweaks/presentation/build.gradle.kts index 67796677e..5990ccfc8 100644 --- a/feature/tweaks/presentation/build.gradle.kts +++ b/feature/tweaks/presentation/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { api(libs.ktor.client.core) implementation(libs.touchlab.kermit) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt index d07194b8f..86a36ee09 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/licenses/LicensesRoot.kt @@ -3,18 +3,25 @@ package zed.rainxch.tweaks.presentation.licenses import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler @@ -23,46 +30,30 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.jetbrains.compose.resources.stringResource +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res -import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_intro_body -import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_intro_title -import zed.rainxch.githubstore.core.presentation.res.tweaks_licenses_title import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold -private data class Library( +@Serializable +private data class LibraryEntry( val name: String, val license: String, val url: String, ) -private val LIBRARIES: List = listOf( - Library("Kotlin", "Apache-2.0", "https://github.com/JetBrains/kotlin"), - Library("Compose Multiplatform", "Apache-2.0", "https://github.com/JetBrains/compose-multiplatform"), - Library("Jetpack Compose", "Apache-2.0", "https://developer.android.com/jetpack/compose"), - Library("Ktor", "Apache-2.0", "https://github.com/ktorio/ktor"), - Library("Room", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/room"), - Library("Koin", "Apache-2.0", "https://github.com/InsertKoinIO/koin"), - Library("kotlinx.serialization", "Apache-2.0", "https://github.com/Kotlin/kotlinx.serialization"), - Library("kotlinx.coroutines", "Apache-2.0", "https://github.com/Kotlin/kotlinx.coroutines"), - Library("kotlinx.datetime", "Apache-2.0", "https://github.com/Kotlin/kotlinx-datetime"), - Library("DataStore", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/datastore"), - Library("Landscapist", "Apache-2.0", "https://github.com/skydoves/landscapist"), - Library("Kermit", "Apache-2.0", "https://github.com/touchlab/Kermit"), - Library("MOKO Permissions", "Apache-2.0", "https://github.com/icerockdev/moko-permissions"), - Library("Navigation Compose", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/navigation"), - Library("multiplatform-markdown-renderer", "Apache-2.0", "https://github.com/mikepenz/multiplatform-markdown-renderer"), - Library("Shizuku", "Apache-2.0", "https://github.com/RikkaApps/Shizuku"), - Library("WorkManager", "Apache-2.0", "https://developer.android.com/jetpack/androidx/releases/work"), - Library("KSafe", "Apache-2.0", "https://github.com/Anifantakis/KSafe"), - Library("Geist (Vercel)", "OFL-1.1", "https://github.com/vercel/geist-font"), - Library("Material Icons Extended", "Apache-2.0", "https://fonts.google.com/icons"), +@Serializable +private data class LicensesPayload( + val libraries: List = emptyList(), ) +private val licensesJson = Json { ignoreUnknownKeys = true } + +@OptIn(org.jetbrains.compose.resources.ExperimentalResourceApi::class) @Composable fun LicensesRoot( onNavigateBack: () -> Unit, @@ -72,8 +63,22 @@ fun LicensesRoot( val snackbarState = remember { SnackbarHostState() } val uriHandler = LocalUriHandler.current + var libraries by remember { mutableStateOf?>(null) } + var loadError by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + runCatching { + val bytes = Res.readBytes("files/licenses.json") + licensesJson.decodeFromString( + LicensesPayload.serializer(), + bytes.decodeToString(), + ).libraries + }.onSuccess { libraries = it } + .onFailure { loadError = true } + } + TweaksSubScreenScaffold( - title = stringResource(Res.string.tweaks_licenses_title), + title = "Open source licenses", onNavigateBack = onNavigateBack, snackbarState = snackbarState, restartReasons = state.needsRestartReasons, @@ -90,13 +95,13 @@ fun LicensesRoot( ) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = stringResource(Res.string.tweaks_licenses_intro_title), + text = "GitHub Store stands on these libraries.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) Spacer(Modifier.height(4.dp)) Text( - text = stringResource(Res.string.tweaks_licenses_intro_body), + text = "Tap any entry to open its project page.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -105,19 +110,46 @@ fun LicensesRoot( Spacer(Modifier.height(12.dp)) } - LIBRARIES.forEach { library -> - item(key = "lib_${library.name}") { - LibraryRow(library = library, onClick = { - runCatching { uriHandler.openUri(library.url) } - }) - Spacer(Modifier.height(8.dp)) + val list = libraries + when { + list == null && !loadError -> { + item(key = "loading") { + Box( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 48.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + } + loadError -> { + item(key = "error") { + Text( + text = "Couldn't load licenses.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(16.dp), + ) + } + } + list != null -> { + list.forEach { library -> + item(key = "lib_${library.name}") { + LibraryRow(library = library, onClick = { + runCatching { uriHandler.openUri(library.url) } + }) + Spacer(Modifier.height(8.dp)) + } + } } } } } @Composable -private fun LibraryRow(library: Library, onClick: () -> Unit) { +private fun LibraryRow(library: LibraryEntry, onClick: () -> Unit) { Surface( modifier = Modifier .fillMaxWidth() From f6488359b227aeeec7b64c65ed799c52e6e521cf Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 06:46:53 +0500 Subject: [PATCH 128/172] build: add printRuntimeDependencies audit task --- composeApp/build.gradle.kts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d13e44eb5..baf533828 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -170,3 +170,26 @@ compose.desktop { } } } + +// Maintainer helper: prints sorted unique runtime dependencies of the Android release variant. +// Use to audit the curated licenses.json after adding/removing deps. +// ./gradlew :composeApp:printRuntimeDependencies +tasks.register("printRuntimeDependencies") { + description = "Lists all runtime dependencies for licenses.json maintenance." + group = "verification" + val coordinatesProvider: Provider> = project.provider { + val cfg = configurations.findByName("releaseRuntimeClasspath") + ?: configurations.findByName("androidReleaseRuntimeClasspath") + ?: error("No release runtime classpath configuration found.") + cfg.incoming.resolutionResult.allComponents + .map { it.id } + .filterIsInstance() + .map { "${it.group}:${it.module}:${it.version}" } + .distinct() + .sorted() + } + notCompatibleWithConfigurationCache("Resolves Android variant configurations at execution time.") + doLast { + coordinatesProvider.get().forEach { println(it) } + } +} From 464813d2048c78e0f00533613d694fc84d07d6f2 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 13:46:40 +0500 Subject: [PATCH 129/172] core: lift hub primitives from tweaks to core --- .../components/hub/GhsEntryRow.kt | 53 ++++++++++++------- .../components/hub/GhsSectionHeader.kt | 6 +-- .../presentation/theme/tokens/GhsAccents.kt | 4 +- .../rainxch/tweaks/presentation/TweaksRoot.kt | 36 ++++++------- .../presentation/appinfo/TweaksAppInfoRoot.kt | 26 ++++----- .../components/sections/Appearance.kt | 1 - .../components/sections/Installation.kt | 1 - .../components/sections/Translation.kt | 1 - .../presentation/privacy/TweaksPrivacyRoot.kt | 4 +- .../presentation/sources/TweaksSourcesRoot.kt | 6 +-- 10 files changed, 75 insertions(+), 63 deletions(-) rename feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt => core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsEntryRow.kt (75%) rename feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt => core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsSectionHeader.kt (82%) rename feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt => core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/GhsAccents.kt (85%) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsEntryRow.kt similarity index 75% rename from feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt rename to core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsEntryRow.kt index fe0c6e5a6..73159790b 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksEntryRow.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsEntryRow.kt @@ -1,8 +1,9 @@ -package zed.rainxch.tweaks.presentation.components +package zed.rainxch.core.presentation.components.hub import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,8 +32,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import zed.rainxch.core.presentation.theme.tokens.Radii +@OptIn(ExperimentalFoundationApi::class) @Composable -fun TweaksEntryRow( +fun GhsEntryRow( title: String, icon: ImageVector, onClick: () -> Unit, @@ -40,24 +42,35 @@ fun TweaksEntryRow( subtitle: String? = null, badge: (@Composable () -> Unit)? = null, accentColor: Color = Color.Unspecified, + onLongClick: (() -> Unit)? = null, + destructive: Boolean = false, + trailingChevron: Boolean = true, ) { val effectiveAccent = - if (accentColor == Color.Unspecified) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - accentColor + when { + destructive -> MaterialTheme.colorScheme.error + accentColor == Color.Unspecified -> MaterialTheme.colorScheme.onSurfaceVariant + else -> accentColor } val tileBackground = - if (accentColor == Color.Unspecified) { - MaterialTheme.colorScheme.surfaceContainerHigh + when { + destructive -> MaterialTheme.colorScheme.error.copy(alpha = 0.14f) + accentColor == Color.Unspecified -> MaterialTheme.colorScheme.surfaceContainerHigh + else -> accentColor.copy(alpha = 0.14f) + } + val titleColor = + if (destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface + val clickMod = + if (onLongClick != null) { + Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick) } else { - accentColor.copy(alpha = 0.14f) + Modifier.combinedClickable(onClick = onClick) } Surface( modifier = modifier .fillMaxWidth() .clip(Radii.row) - .clickable(onClick = onClick) + .then(clickMod) .semantics { role = Role.Button contentDescription = buildString { @@ -107,7 +120,7 @@ fun TweaksEntryRow( style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.SemiBold, ), - color = MaterialTheme.colorScheme.onSurface, + color = titleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -126,18 +139,20 @@ fun TweaksEntryRow( badge() } - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp), - ) + if (trailingChevron) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp), + ) + } } } } @Composable -fun TweaksEntryBadge(text: String) { +fun GhsEntryBadge(text: String) { Surface( shape = Radii.chip, color = MaterialTheme.colorScheme.tertiaryContainer, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsSectionHeader.kt similarity index 82% rename from feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt rename to core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsSectionHeader.kt index 7c495d956..4ed5c0bc5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/SectionText.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/hub/GhsSectionHeader.kt @@ -1,4 +1,4 @@ -package zed.rainxch.tweaks.presentation.components +package zed.rainxch.core.presentation.components.hub import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,9 +13,9 @@ import androidx.compose.ui.unit.sp import zed.rainxch.core.presentation.vocabulary.Squiggle @Composable -fun SectionHeader(text: String) { +fun GhsSectionHeader(text: String, modifier: Modifier = Modifier) { Column( - modifier = Modifier.padding(start = 4.dp, top = 12.dp, bottom = 4.dp), + modifier = modifier.padding(start = 4.dp, top = 12.dp, bottom = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/GhsAccents.kt similarity index 85% rename from feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt rename to core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/GhsAccents.kt index 60cc404e1..6c0617ed2 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksAccents.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/GhsAccents.kt @@ -1,8 +1,8 @@ -package zed.rainxch.tweaks.presentation.components +package zed.rainxch.core.presentation.theme.tokens import androidx.compose.ui.graphics.Color -object TweaksAccents { +object GhsAccents { val Lavender = Color(0xFFB19CD9) val Mint = Color(0xFF88C9BF) val Sky = Color(0xFF8FB8E0) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 653041c83..351a4b692 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -63,9 +63,9 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.components.RestartBanner -import zed.rainxch.tweaks.presentation.components.SectionHeader -import zed.rainxch.tweaks.presentation.components.TweaksAccents -import zed.rainxch.tweaks.presentation.components.TweaksEntryRow +import zed.rainxch.core.presentation.components.hub.GhsEntryRow +import zed.rainxch.core.presentation.components.hub.GhsSectionHeader +import zed.rainxch.core.presentation.theme.tokens.GhsAccents import zed.rainxch.tweaks.presentation.components.TweaksSearchField import zed.rainxch.tweaks.presentation.feedback.components.FeedbackBottomSheet import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel @@ -216,14 +216,14 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.Palette, onClick = onNavigateToAppearance, - accent = TweaksAccents.Lavender, + accent = GhsAccents.Lavender, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_language), subtitle = tapToManage, icon = Icons.Outlined.Translate, onClick = onNavigateToLanguage, - accent = TweaksAccents.Mint, + accent = GhsAccents.Mint, ), ), ), @@ -235,21 +235,21 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.Wifi, onClick = onNavigateToConnection, - accent = TweaksAccents.Sky, + accent = GhsAccents.Sky, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_sources), subtitle = tapToManage, icon = Icons.Outlined.Hub, onClick = onNavigateToSources, - accent = TweaksAccents.Blush, + accent = GhsAccents.Blush, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_translation), subtitle = tapToManage, icon = Icons.Outlined.GTranslate, onClick = onNavigateToTranslation, - accent = TweaksAccents.Peach, + accent = GhsAccents.Peach, ), ), ), @@ -261,14 +261,14 @@ fun TweaksHubScreen( subtitle = tapToManage, icon = Icons.Outlined.InstallMobile, onClick = onNavigateToInstallMethod, - accent = TweaksAccents.Sage, + accent = GhsAccents.Sage, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_updates), subtitle = tapToManage, icon = Icons.Outlined.Update, onClick = onNavigateToUpdates, - accent = TweaksAccents.Amber, + accent = GhsAccents.Amber, ), ), ), @@ -280,21 +280,21 @@ fun TweaksHubScreen( subtitle = state.cacheSize.ifBlank { tapToManage }, icon = Icons.Outlined.Inventory2, onClick = onNavigateToStorage, - accent = TweaksAccents.Periwinkle, + accent = GhsAccents.Periwinkle, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_privacy), subtitle = tapToManage, icon = Icons.Outlined.PrivacyTip, onClick = onNavigateToPrivacy, - accent = TweaksAccents.Rose, + accent = GhsAccents.Rose, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_access_tokens), subtitle = tapToManage, icon = Icons.Outlined.VpnKey, onClick = onNavigateToHostTokens, - accent = TweaksAccents.Gold, + accent = GhsAccents.Gold, ), ), ), @@ -306,14 +306,14 @@ fun TweaksHubScreen( subtitle = state.versionName.ifBlank { "—" }, icon = Icons.Outlined.Info, onClick = onNavigateToAppInfo, - accent = TweaksAccents.Aqua, + accent = GhsAccents.Aqua, ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_feedback), subtitle = stringResource(Res.string.feedback_hub_subtitle), icon = Icons.Outlined.Feedback, onClick = onSendFeedbackClick, - accent = TweaksAccents.Tan, + accent = GhsAccents.Tan, ), ), ), @@ -405,12 +405,12 @@ fun TweaksHubScreen( blocks.forEachIndexed { idx, block -> item(key = "block_header_${block.title}") { if (idx > 0) Spacer(Modifier.height(24.dp)) - SectionHeader(text = block.title) + GhsSectionHeader(text = block.title) Spacer(Modifier.height(8.dp)) } block.entries.forEach { entry -> item(key = "entry_${block.title}_${entry.title}") { - TweaksEntryRow( + GhsEntryRow( title = entry.title, subtitle = entry.subtitle, icon = entry.icon, @@ -424,7 +424,7 @@ fun TweaksHubScreen( } else { filteredBlocks.flatMap { it.entries }.forEach { entry -> item(key = "search_entry_${entry.title}") { - TweaksEntryRow( + GhsEntryRow( title = entry.title, subtitle = entry.subtitle, icon = entry.icon, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index dbbb30e53..5cb5a1ee5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -48,12 +48,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii -import zed.rainxch.tweaks.presentation.components.TweaksAccents +import zed.rainxch.core.presentation.theme.tokens.GhsAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant -import zed.rainxch.tweaks.presentation.components.SectionHeader +import zed.rainxch.core.presentation.components.hub.GhsSectionHeader import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_app_name import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_business_cta import zed.rainxch.githubstore.core.presentation.res.tweaks_app_info_community_business_subtitle @@ -112,7 +112,7 @@ fun TweaksAppInfoRoot( } item(key = "community_section_header") { - SectionHeader(text = stringResource(Res.string.tweaks_app_info_community_section)) + GhsSectionHeader(text = stringResource(Res.string.tweaks_app_info_community_section)) Spacer(Modifier.height(8.dp)) } @@ -134,7 +134,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.NewReleases, title = stringResource(Res.string.tweaks_app_info_whats_new_title), subtitle = stringResource(Res.string.tweaks_app_info_whats_new_subtitle), - accent = TweaksAccents.Peach, + accent = GhsAccents.Peach, onClick = onNavigateToWhatsNewHistory, ) Spacer(Modifier.height(8.dp)) @@ -145,7 +145,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.Code, title = stringResource(Res.string.tweaks_app_info_licenses_title), subtitle = stringResource(Res.string.tweaks_app_info_licenses_subtitle), - accent = TweaksAccents.Sage, + accent = GhsAccents.Sage, onClick = onNavigateToLicenses, ) Spacer(Modifier.height(8.dp)) @@ -156,7 +156,7 @@ fun TweaksAppInfoRoot( icon = Icons.Outlined.Description, title = stringResource(Res.string.tweaks_app_info_privacy_policy_title), subtitle = stringResource(Res.string.tweaks_app_info_privacy_policy_subtitle), - accent = TweaksAccents.Rose, + accent = GhsAccents.Rose, onClick = { runCatching { uriHandler.openUri(PRIVACY_POLICY_URL) } }, @@ -169,7 +169,7 @@ fun TweaksAppInfoRoot( icon = Icons.AutoMirrored.Outlined.OpenInNew, title = stringResource(Res.string.tweaks_app_info_source_code_title), subtitle = stringResource(Res.string.tweaks_app_info_source_code_subtitle), - accent = TweaksAccents.Aqua, + accent = GhsAccents.Aqua, onClick = { runCatching { uriHandler.openUri(SOURCE_CODE_URL) } }, @@ -281,21 +281,21 @@ private fun CommunityCard( SocialTile( label = "Telegram", icon = Icons.AutoMirrored.Filled.Send, - accent = TweaksAccents.Sky, + accent = GhsAccents.Sky, onClick = onTelegram, modifier = Modifier.weight(1f), ) SocialTile( label = "Discord", icon = Icons.Outlined.Forum, - accent = TweaksAccents.Periwinkle, + accent = GhsAccents.Periwinkle, onClick = onDiscord, modifier = Modifier.weight(1f), ) SocialTile( label = "Mastodon", icon = Icons.Outlined.AlternateEmail, - accent = TweaksAccents.Lavender, + accent = GhsAccents.Lavender, onClick = onMastodon, modifier = Modifier.weight(1f), ) @@ -308,21 +308,21 @@ private fun CommunityCard( SocialTile( label = "Reddit", icon = Icons.Outlined.Public, - accent = TweaksAccents.Peach, + accent = GhsAccents.Peach, onClick = onReddit, modifier = Modifier.weight(1f), ) SocialTile( label = "GitHub", icon = Icons.Outlined.Code, - accent = TweaksAccents.Tan, + accent = GhsAccents.Tan, onClick = onGithub, modifier = Modifier.weight(1f), ) SocialTile( label = "Website", icon = Icons.Outlined.Language, - accent = TweaksAccents.Sage, + accent = GhsAccents.Sage, onClick = onWebsite, modifier = Modifier.weight(1f), ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index 835fdedba..36c547885 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -61,7 +61,6 @@ import zed.rainxch.core.presentation.utils.toTokenPalette import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader import zed.rainxch.tweaks.presentation.components.ToggleSettingCard fun LazyListScope.appearanceSection( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index 29e57f5b1..debd6968b 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -61,7 +61,6 @@ import zed.rainxch.core.presentation.components.inputs.GhsTextField import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun LazyListScope.installationSection( diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt index 616edb72c..1936c9ade 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt @@ -58,7 +58,6 @@ import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformat import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState -import zed.rainxch.tweaks.presentation.components.SectionHeader fun LazyListScope.translationSection( state: TweaksState, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt index 69e7711b0..ff5466ec0 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -40,7 +40,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.components.overlays.GhsConfirmDialog import zed.rainxch.core.presentation.theme.tokens.Radii -import zed.rainxch.tweaks.presentation.components.TweaksAccents +import zed.rainxch.core.presentation.theme.tokens.GhsAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.tweaks_entry_privacy import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_browsing_history_section @@ -144,7 +144,7 @@ fun TweaksPrivacyRoot( icon = Icons.Outlined.VisibilityOff, title = stringResource(Res.string.tweaks_privacy_hidden_repos_title), subtitle = stringResource(Res.string.tweaks_privacy_hidden_repos_body), - accent = TweaksAccents.Periwinkle, + accent = GhsAccents.Periwinkle, onClick = onNavigateToHiddenRepositories, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt index 00023bbe8..dfe0e8550 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -40,7 +40,7 @@ import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.core.presentation.theme.tokens.Radii -import zed.rainxch.tweaks.presentation.components.TweaksAccents +import zed.rainxch.core.presentation.theme.tokens.GhsAccents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.custom_forges_count import zed.rainxch.githubstore.core.presentation.res.custom_forges_entry_label @@ -106,7 +106,7 @@ fun TweaksSourcesRoot( icon = Icons.Outlined.NetworkCheck, title = stringResource(Res.string.tweaks_sources_github_mirror_title), subtitle = stringResource(Res.string.tweaks_sources_mirror_default), - accent = TweaksAccents.Sky, + accent = GhsAccents.Sky, onClick = onNavigateToMirrorPicker, ) Spacer(Modifier.height(8.dp)) @@ -122,7 +122,7 @@ fun TweaksSourcesRoot( } else { pluralStringResource(Res.plurals.custom_forges_count, count, count) }, - accent = TweaksAccents.Mint, + accent = GhsAccents.Mint, onClick = { viewModel.onAction(TweaksAction.OnOpenCustomForgesDialog) }, ) } From eda3bd21ed18ef2b855cc03b2e25cfed5e7a59ad Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 13:50:13 +0500 Subject: [PATCH 130/172] feat(profile): hero card + sectioned hub redesign --- .../composeResources/values/strings.xml | 3 + .../profile/presentation/ProfileRoot.kt | 15 +- .../components/ProfileSections.kt | 389 ++++++++++++++++++ .../components/sections/Account.kt | 70 ---- .../components/sections/AccountSection.kt | 252 ------------ .../components/sections/Options.kt | 185 --------- .../components/sections/ProfileSection.kt | 30 -- 7 files changed, 394 insertions(+), 550 deletions(-) create mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt delete mode 100644 feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/ProfileSection.kt diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 7b43a0e96..7add4f8ed 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1064,6 +1064,9 @@ App Installs & updates Look & feel + Library + Updates + Account Appearance Language Connection diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 2c8545a4d..48cc0260e 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -38,8 +38,7 @@ import zed.rainxch.githubstore.core.presentation.res.profile_title import zed.rainxch.githubstore.core.presentation.res.proxy_saved import zed.rainxch.githubstore.core.presentation.res.seen_history_cleared import zed.rainxch.profile.presentation.components.LogoutDialog -import zed.rainxch.profile.presentation.components.sections.logout -import zed.rainxch.profile.presentation.components.sections.profile +import zed.rainxch.profile.presentation.components.profileSections @Composable fun ProfileRoot( @@ -205,22 +204,12 @@ fun ProfileScreen( .padding(16.dp) .arrowKeyScroll(listState, autoFocus = true), ) { - profile( + profileSections( state = state, hasUnreadAnnouncements = hasUnreadAnnouncements, onAction = onAction, ) - item { - Spacer(Modifier.height(32.dp)) - } - - if (state.isUserLoggedIn) { - logout( - onAction = onAction, - ) - } - item { Spacer(Modifier.height(bottomNavHeight + 32.dp)) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt new file mode 100644 index 000000000..8e584599c --- /dev/null +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt @@ -0,0 +1,389 @@ +package zed.rainxch.profile.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.outlined.Campaign +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Schedule +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.components.buttons.GhsButton +import zed.rainxch.core.presentation.components.buttons.GhsButtonSize +import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant +import zed.rainxch.core.presentation.components.hub.GhsEntryRow +import zed.rainxch.core.presentation.components.hub.GhsSectionHeader +import zed.rainxch.core.presentation.theme.tokens.GhsAccents +import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.formatCount +import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.profile.presentation.ProfileAction +import zed.rainxch.profile.presentation.ProfileState + +fun LazyListScope.profileSections( + state: ProfileState, + hasUnreadAnnouncements: Boolean, + onAction: (ProfileAction) -> Unit, +) { + item(key = "identity") { + HeroIdentityCard(state = state, onAction = onAction) + } + + if (state.isUserLoggedIn) { + item(key = "library_header") { + Spacer(Modifier.height(8.dp)) + GhsSectionHeader(text = stringResource(Res.string.profile_section_library)) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_stars") { + GhsEntryRow( + title = stringResource(Res.string.stars), + subtitle = stringResource(Res.string.profile_stars_description), + icon = Icons.Outlined.Star, + accentColor = GhsAccents.Gold, + onClick = { onAction(ProfileAction.OnStarredReposClick) }, + ) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_favourites") { + GhsEntryRow( + title = stringResource(Res.string.favourites), + subtitle = stringResource(Res.string.profile_favourites_description), + icon = Icons.Outlined.Favorite, + accentColor = GhsAccents.Rose, + onClick = { onAction(ProfileAction.OnFavouriteReposClick) }, + ) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_recent") { + GhsEntryRow( + title = stringResource(Res.string.recently_viewed), + subtitle = stringResource(Res.string.profile_recently_viewed_description), + icon = Icons.Outlined.Schedule, + accentColor = GhsAccents.Sky, + onClick = { onAction(ProfileAction.OnRecentlyViewedClick) }, + ) + } + } + + item(key = "updates_header") { + Spacer(Modifier.height(8.dp)) + GhsSectionHeader(text = stringResource(Res.string.profile_section_updates)) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_whats_new") { + GhsEntryRow( + title = stringResource(Res.string.whats_new_title), + subtitle = stringResource(Res.string.whats_new_profile_description), + icon = Icons.Outlined.Campaign, + accentColor = GhsAccents.Mint, + onClick = { onAction(ProfileAction.OnWhatsNewClick) }, + onLongClick = { onAction(ProfileAction.OnWhatsNewLongClick) }, + ) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_announcements") { + GhsEntryRow( + title = stringResource(Res.string.announcements_title), + subtitle = stringResource(Res.string.announcements_profile_description), + icon = Icons.Outlined.Notifications, + accentColor = GhsAccents.Lavender, + onClick = { onAction(ProfileAction.OnAnnouncementsClick) }, + onLongClick = { onAction(ProfileAction.OnAnnouncementsLongClick) }, + badge = if (hasUnreadAnnouncements) { + { UnreadDot() } + } else { + null + }, + ) + } + + item(key = "app_header") { + Spacer(Modifier.height(8.dp)) + GhsSectionHeader(text = stringResource(Res.string.section_app_block)) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_tweaks") { + GhsEntryRow( + title = stringResource(Res.string.tweaks_title), + subtitle = stringResource(Res.string.profile_tweaks_description), + icon = Icons.Outlined.Tune, + accentColor = GhsAccents.Sage, + onClick = { onAction(ProfileAction.OnTweaksClick) }, + ) + } + + if (state.isUserLoggedIn) { + item(key = "account_header") { + Spacer(Modifier.height(8.dp)) + GhsSectionHeader(text = stringResource(Res.string.profile_section_account)) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_logout") { + GhsEntryRow( + title = stringResource(Res.string.logout), + icon = Icons.AutoMirrored.Filled.Logout, + destructive = true, + trailingChevron = false, + onClick = { onAction(ProfileAction.OnLogoutClick) }, + ) + } + } +} + +@Composable +private fun HeroIdentityCard( + state: ProfileState, + onAction: (ProfileAction) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (state.userProfile == null) { + SignedOutContent(onAction = onAction) + } else { + SignedInContent(state = state, onAction = onAction) + } + } + } +} + +@Composable +private fun SignedOutContent(onAction: (ProfileAction) -> Unit) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box( + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.AccountCircle, + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(Res.string.profile_sign_in_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(Res.string.profile_sign_in_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(4.dp)) + GhsButton( + onClick = { onAction(ProfileAction.OnLoginClick) }, + label = stringResource(Res.string.profile_login), + variant = GhsButtonVariant.Primary, + size = GhsButtonSize.Lg, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun SignedInContent( + state: ProfileState, + onAction: (ProfileAction) -> Unit, +) { + val profile = state.userProfile ?: return + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + GitHubStoreImage( + imageModel = { profile.imageUrl }, + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + extractDominantFor = profile.imageUrl, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + val displayName = profile.name.takeIf { it.isNotBlank() } ?: profile.username + Text( + text = displayName, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = "@${profile.username}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> + Spacer(Modifier.height(2.dp)) + Text( + text = bio, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + Spacer(Modifier.height(4.dp)) + MetricsStrip( + repos = profile.repositoryCount, + followers = profile.followers, + following = profile.following, + onReposClick = { onAction(ProfileAction.OnRepositoriesClick(profile.username)) }, + ) +} + +@Composable +private fun MetricsStrip( + repos: Int, + followers: Int, + following: Int, + onReposClick: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Metric( + value = formatCount(repos), + label = stringResource(Res.string.profile_repos), + modifier = Modifier + .weight(1f) + .clip(Radii.chip) + .clickable(onClick = onReposClick) + .padding(vertical = 6.dp), + ) + MetricDivider() + Metric( + value = formatCount(followers), + label = stringResource(Res.string.followers), + modifier = Modifier + .weight(1f) + .padding(vertical = 6.dp), + ) + MetricDivider() + Metric( + value = formatCount(following), + label = stringResource(Res.string.following), + modifier = Modifier + .weight(1f) + .padding(vertical = 6.dp), + ) + } +} + +@Composable +private fun Metric( + value: String, + label: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = value, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun MetricDivider() { + Box( + modifier = Modifier + .width(1.dp) + .height(28.dp) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), + ) +} + +@Composable +private fun UnreadDot() { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.error), + ) +} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt deleted file mode 100644 index 047cfb481..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Account.kt +++ /dev/null @@ -1,70 +0,0 @@ -package zed.rainxch.profile.presentation.components.sections - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.presentation.ProfileAction - -fun LazyListScope.logout(onAction: (ProfileAction) -> Unit) { - item { - Spacer(Modifier.height(8.dp)) - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(50), - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.error.copy(alpha = 0.55f), - ), - onClick = { onAction(ProfileAction.OnLogoutClick) }, - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 14.dp, horizontal = 18.dp), - contentAlignment = Alignment.Center, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Logout, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.error, - ) - Text( - text = stringResource(Res.string.logout), - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.error, - ) - } - } - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt deleted file mode 100644 index 85bb083f6..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/AccountSection.kt +++ /dev/null @@ -1,252 +0,0 @@ -package zed.rainxch.profile.presentation.components.sections - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.domain.model.UserProfile -import zed.rainxch.core.presentation.components.GitHubStoreImage -import zed.rainxch.core.presentation.components.buttons.GhsButton -import zed.rainxch.core.presentation.components.buttons.GhsButtonSize -import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant -import zed.rainxch.core.presentation.theme.GithubStoreTheme -import zed.rainxch.core.presentation.theme.tokens.Radii -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.presentation.ProfileAction -import zed.rainxch.profile.presentation.ProfileState - -fun LazyListScope.accountSection( - state: ProfileState, - onAction: (ProfileAction) -> Unit, -) { - item { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (state.userProfile == null) { - Box( - modifier = Modifier - .size(112.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Filled.AccountCircle, - contentDescription = null, - modifier = Modifier.size(76.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } else { - GitHubStoreImage( - imageModel = { state.userProfile.imageUrl }, - modifier = Modifier - .size(112.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), - extractDominantFor = state.userProfile.imageUrl, - ) - } - - Spacer(Modifier.height(8.dp)) - - if (state.userProfile != null) { - val displayName = state.userProfile.name.takeIf { it.isNotBlank() } - ?: state.userProfile.username - Text( - text = displayName, - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - ), - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = "@${state.userProfile.username}", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - state.userProfile.bio?.takeIf { it.isNotBlank() }?.let { bio -> - Spacer(Modifier.height(4.dp)) - Text( - text = bio, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - StatPill( - label = stringResource(Res.string.profile_repos), - value = state.userProfile.repositoryCount.toString(), - modifier = Modifier.weight(1f), - onClick = { - onAction(ProfileAction.OnRepositoriesClick(state.userProfile.username)) - }, - ) - StatPill( - label = stringResource(Res.string.followers), - value = state.userProfile.followers.toString(), - modifier = Modifier.weight(1f), - ) - StatPill( - label = stringResource(Res.string.following), - value = state.userProfile.following.toString(), - modifier = Modifier.weight(1f), - ) - } - } else { - Text( - text = stringResource(Res.string.profile_sign_in_title), - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - ), - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(Res.string.profile_sign_in_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(16.dp)) - GhsButton( - onClick = { onAction(ProfileAction.OnLoginClick) }, - label = stringResource(Res.string.profile_login), - variant = GhsButtonVariant.Primary, - size = GhsButtonSize.Lg, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - ) - } - } - } -} - -@Composable -private fun StatPill( - label: String, - value: String, - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, -) { - Surface( - modifier = modifier, - shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), - ), - onClick = { onClick?.invoke() }, - enabled = onClick != null, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 6.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = value, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -fun AccountSectionPreview() { - GithubStoreTheme { - LazyColumn { - accountSection(state = ProfileState(), onAction = { }) - } - } -} - -@Preview(showBackground = true) -@Composable -fun AccountSectionUserPreview() { - GithubStoreTheme { - LazyColumn { - accountSection( - state = ProfileState( - userProfile = UserProfile( - id = 1, - imageUrl = "", - name = "Octocat", - username = "the_octocat", - bio = "Language Savant.", - repositoryCount = 8, - followers = 21900, - following = 9, - ), - ), - onAction = { }, - ) - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt deleted file mode 100644 index 38c104823..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Options.kt +++ /dev/null @@ -1,185 +0,0 @@ -package zed.rainxch.profile.presentation.components.sections - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight -import androidx.compose.material.icons.filled.Campaign -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.Tune -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.stringResource -import zed.rainxch.core.presentation.theme.tokens.Radii -import zed.rainxch.githubstore.core.presentation.res.* -import zed.rainxch.profile.presentation.ProfileAction - -fun LazyListScope.options( - isUserLoggedIn: Boolean, - hasUnreadAnnouncements: Boolean, - onAction: (ProfileAction) -> Unit, -) { - item { - if (isUserLoggedIn) { - OptionCard( - icon = Icons.Default.Star, - label = stringResource(Res.string.stars), - description = stringResource(Res.string.profile_stars_description), - onClick = { onAction(ProfileAction.OnStarredReposClick) }, - ) - Spacer(Modifier.height(8.dp)) - } - - OptionCard( - icon = Icons.Default.Favorite, - label = stringResource(Res.string.favourites), - description = stringResource(Res.string.profile_favourites_description), - onClick = { onAction(ProfileAction.OnFavouriteReposClick) }, - ) - Spacer(Modifier.height(8.dp)) - - OptionCard( - icon = Icons.Default.Schedule, - label = stringResource(Res.string.recently_viewed), - description = stringResource(Res.string.profile_recently_viewed_description), - onClick = { onAction(ProfileAction.OnRecentlyViewedClick) }, - ) - Spacer(Modifier.height(8.dp)) - - OptionCard( - icon = Icons.Default.Campaign, - label = stringResource(Res.string.whats_new_title), - description = stringResource(Res.string.whats_new_profile_description), - onClick = { onAction(ProfileAction.OnWhatsNewClick) }, - onLongClick = { onAction(ProfileAction.OnWhatsNewLongClick) }, - ) - Spacer(Modifier.height(8.dp)) - - OptionCard( - icon = Icons.Default.Notifications, - label = stringResource(Res.string.announcements_title), - description = stringResource(Res.string.announcements_profile_description), - onClick = { onAction(ProfileAction.OnAnnouncementsClick) }, - onLongClick = { onAction(ProfileAction.OnAnnouncementsLongClick) }, - hasBadge = hasUnreadAnnouncements, - ) - Spacer(Modifier.height(8.dp)) - - OptionCard( - icon = Icons.Default.Tune, - label = stringResource(Res.string.tweaks_title), - description = stringResource(Res.string.profile_tweaks_description), - onClick = { onAction(ProfileAction.OnTweaksClick) }, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun OptionCard( - icon: ImageVector, - label: String, - description: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - onLongClick: (() -> Unit)? = null, - hasBadge: Boolean = false, -) { - val clickMod = if (onLongClick != null) { - Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick) - } else { - Modifier.combinedClickable(onClick = onClick) - } - Surface( - modifier = modifier, - shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), - ), - ) { - Row( - modifier = clickMod.padding(horizontal = 14.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Box( - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary, - ) - if (hasBadge) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(8.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.error), - ) - } - } - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = label, - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - Icon( - imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } -} diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/ProfileSection.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/ProfileSection.kt deleted file mode 100644 index 01381c651..000000000 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/ProfileSection.kt +++ /dev/null @@ -1,30 +0,0 @@ -package zed.rainxch.profile.presentation.components.sections - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import zed.rainxch.profile.presentation.ProfileAction -import zed.rainxch.profile.presentation.ProfileState - -fun LazyListScope.profile( - state: ProfileState, - hasUnreadAnnouncements: Boolean, - onAction: (ProfileAction) -> Unit, -) { - accountSection( - state = state, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(20.dp)) - } - - options( - isUserLoggedIn = state.isUserLoggedIn, - hasUnreadAnnouncements = hasUnreadAnnouncements, - onAction = onAction, - ) -} From 4483bdb13d3680f24ae42a742525cb86980dc71c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 13:50:14 +0500 Subject: [PATCH 131/172] i18n(profile): translate Library/Updates/Account to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 3 +++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 3 +++ .../src/commonMain/composeResources/values-es/strings-es.xml | 3 +++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 3 +++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 3 +++ .../src/commonMain/composeResources/values-it/strings-it.xml | 3 +++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 3 +++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 3 +++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 3 +++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 3 +++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 3 +++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 3 +++ 12 files changed, 36 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 51d26fc8f..a194f2105 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1314,4 +1314,7 @@ استفسارات الأعمال الشراكات، الصحافة، التكاملات. تواصل + المكتبة + التحديثات + الحساب diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 2f9fa0de8..08ff1d1a1 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1276,4 +1276,7 @@ ব্যবসায়িক অনুসন্ধান অংশীদারি, প্রেস, ইন্টিগ্রেশন। যোগাযোগ + লাইব্রেরি + আপডেট + অ্যাকাউন্ট diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index e0491bd2f..b1ef2ebeb 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1255,4 +1255,7 @@ Consultas comerciales Alianzas, prensa, integraciones. Contactar + Biblioteca + Actualizaciones + Cuenta diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index fcb47d957..3600ef15e 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1255,4 +1255,7 @@ Demandes professionnelles Partenariats, presse, intégrations. Contacter + Bibliothèque + Mises à jour + Compte diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 1f31635ee..0a973232a 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1291,4 +1291,7 @@ व्यावसायिक पूछताछ साझेदारी, प्रेस, इंटीग्रेशन। संपर्क करें + लाइब्रेरी + अपडेट + अकाउंट diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index d5ac2c63f..d6ae5de78 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1289,4 +1289,7 @@ Richieste commerciali Partnership, stampa, integrazioni. Contattaci + Libreria + Aggiornamenti + Account diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 3668e8b5b..0d7a0d9a0 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1245,4 +1245,7 @@ ビジネスのお問い合わせ パートナーシップ、プレス、連携。 お問い合わせ + ライブラリ + アップデート + アカウント diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index f6d10e3aa..5733e2206 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1275,4 +1275,7 @@ 비즈니스 문의 파트너십, 보도, 연동. 문의 + 라이브러리 + 업데이트 + 계정 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 2f89c88af..913a3fa92 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1284,4 +1284,7 @@ Sprawy biznesowe Współprace, prasa, integracje. Kontakt + Biblioteka + Aktualizacje + Konto diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 1ebd926e9..a5332432e 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1284,4 +1284,7 @@ Деловые запросы Партнёрства, пресса, интеграции. Написать + Библиотека + Обновления + Аккаунт diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index c759e17c1..01ec09973 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1286,4 +1286,7 @@ Ticari sorular Ortaklıklar, basın, entegrasyonlar. İletişim + Kitaplık + Güncellemeler + Hesap diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 98b37b1a8..fdb5f26a1 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1248,4 +1248,7 @@ 商务咨询 合作、媒体、集成。 联系 + 媒体库 + 更新 + 账户 From 79ad4b9fc61eb7e16fe4dadb29ff5e1eb3354c84 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 14:08:51 +0500 Subject: [PATCH 132/172] feat(chrome): unify bottom-nav topbar style --- .../components/chrome/GhsHomeTopBar.kt | 54 +++++++++++++++++++ .../zed/rainxch/apps/presentation/AppsRoot.kt | 48 +++++++---------- .../presentation/components/HomeTopBar.kt | 38 +++---------- .../profile/presentation/ProfileRoot.kt | 19 +------ .../rainxch/search/presentation/SearchRoot.kt | 2 +- 5 files changed, 82 insertions(+), 79 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chrome/GhsHomeTopBar.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chrome/GhsHomeTopBar.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chrome/GhsHomeTopBar.kt new file mode 100644 index 000000000..13f8f756e --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/chrome/GhsHomeTopBar.kt @@ -0,0 +1,54 @@ +package zed.rainxch.core.presentation.components.chrome + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun GhsHomeTopBar( + title: String, + modifier: Modifier = Modifier, + applyStatusBarPadding: Boolean = true, + actions: @Composable (RowScope.() -> Unit)? = null, +) { + val container = modifier + .fillMaxWidth() + .let { if (applyStatusBarPadding) it.statusBarsPadding() else it } + .padding(horizontal = 16.dp, vertical = 12.dp) + Row( + modifier = container, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 26.sp, + ), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f, fill = false), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (actions != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = actions, + ) + } + } +} diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index 7700b0843..a01ae5f76 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -103,6 +103,7 @@ import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.apps.presentation.model.UpdateState import zed.rainxch.core.presentation.components.ExpressiveCard +import zed.rainxch.core.presentation.components.chrome.GhsHomeTopBar import zed.rainxch.core.presentation.components.ScrollbarContainer import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize @@ -249,34 +250,20 @@ fun AppsScreen( Scaffold( topBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding() - .padding(horizontal = 14.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(Res.string.bottom_nav_apps_title), - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - ), - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(start = 4.dp), - ) - Row( - modifier = Modifier - .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.surface) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(50), - ), - verticalAlignment = Alignment.CenterVertically, - ) { + GhsHomeTopBar( + title = stringResource(Res.string.bottom_nav_apps_title), + actions = { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(50), + ), + verticalAlignment = Alignment.CenterVertically, + ) { Box { Box( modifier = Modifier @@ -415,8 +402,9 @@ fun AppsScreen( ) } } - } - } + } + }, + ) }, floatingActionButton = { ExtendedFloatingActionButton( diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt index 45c925762..6201cb2ca 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/components/HomeTopBar.kt @@ -1,41 +1,17 @@ package zed.rainxch.home.presentation.components -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.components.chrome.GhsHomeTopBar import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.home_topbar_discover @Composable -fun HomeTopBar( - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - Text( - text = stringResource(Res.string.home_topbar_discover), - style = MaterialTheme.typography.displaySmall.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - ), - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f), - ) - } +fun HomeTopBar(modifier: Modifier = Modifier) { + GhsHomeTopBar( + title = stringResource(Res.string.home_topbar_discover), + modifier = modifier, + applyStatusBarPadding = false, + ) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 48cc0260e..74ba6f663 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -13,7 +13,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar +import zed.rainxch.core.presentation.components.chrome.GhsHomeTopBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -190,7 +190,7 @@ fun ProfileScreen( ) }, topBar = { - TopAppBar() + GhsHomeTopBar(title = stringResource(Res.string.profile_title)) }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> @@ -217,21 +217,6 @@ fun ProfileScreen( } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun TopAppBar() { - TopAppBar( - title = { - Text( - text = stringResource(Res.string.profile_title), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onBackground, - ) - }, - ) -} @Preview @Composable diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index a6dfde416..48ae51a4b 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -772,7 +772,7 @@ private fun SearchTopbar( Modifier .fillMaxWidth() .statusBarsPadding() - .padding(horizontal = 14.dp, vertical = 10.dp), + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { From acc5e9547873173a3db218dfa1d94a59c8820ec7 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:14:59 +0500 Subject: [PATCH 133/172] core(theme): animate colorScheme transitions --- .../presentation/theme/AnimateColorScheme.kt | 92 +++++++++++++++++++ .../rainxch/core/presentation/theme/Theme.kt | 5 +- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/AnimateColorScheme.kt diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/AnimateColorScheme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/AnimateColorScheme.kt new file mode 100644 index 000000000..262439df6 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/AnimateColorScheme.kt @@ -0,0 +1,92 @@ +package zed.rainxch.core.presentation.theme + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color + +@Composable +fun animateColorScheme( + target: ColorScheme, + spec: AnimationSpec = tween(durationMillis = 420), + label: String = "color_scheme", +): ColorScheme { + val primary by animateColorAsState(target.primary, spec, label = "$label.primary") + val onPrimary by animateColorAsState(target.onPrimary, spec, label = "$label.onPrimary") + val primaryContainer by animateColorAsState(target.primaryContainer, spec, label = "$label.primaryContainer") + val onPrimaryContainer by animateColorAsState(target.onPrimaryContainer, spec, label = "$label.onPrimaryContainer") + val inversePrimary by animateColorAsState(target.inversePrimary, spec, label = "$label.inversePrimary") + val secondary by animateColorAsState(target.secondary, spec, label = "$label.secondary") + val onSecondary by animateColorAsState(target.onSecondary, spec, label = "$label.onSecondary") + val secondaryContainer by animateColorAsState(target.secondaryContainer, spec, label = "$label.secondaryContainer") + val onSecondaryContainer by animateColorAsState(target.onSecondaryContainer, spec, label = "$label.onSecondaryContainer") + val tertiary by animateColorAsState(target.tertiary, spec, label = "$label.tertiary") + val onTertiary by animateColorAsState(target.onTertiary, spec, label = "$label.onTertiary") + val tertiaryContainer by animateColorAsState(target.tertiaryContainer, spec, label = "$label.tertiaryContainer") + val onTertiaryContainer by animateColorAsState(target.onTertiaryContainer, spec, label = "$label.onTertiaryContainer") + val background by animateColorAsState(target.background, spec, label = "$label.background") + val onBackground by animateColorAsState(target.onBackground, spec, label = "$label.onBackground") + val surface by animateColorAsState(target.surface, spec, label = "$label.surface") + val onSurface by animateColorAsState(target.onSurface, spec, label = "$label.onSurface") + val surfaceVariant by animateColorAsState(target.surfaceVariant, spec, label = "$label.surfaceVariant") + val onSurfaceVariant by animateColorAsState(target.onSurfaceVariant, spec, label = "$label.onSurfaceVariant") + val surfaceTint by animateColorAsState(target.surfaceTint, spec, label = "$label.surfaceTint") + val inverseSurface by animateColorAsState(target.inverseSurface, spec, label = "$label.inverseSurface") + val inverseOnSurface by animateColorAsState(target.inverseOnSurface, spec, label = "$label.inverseOnSurface") + val error by animateColorAsState(target.error, spec, label = "$label.error") + val onError by animateColorAsState(target.onError, spec, label = "$label.onError") + val errorContainer by animateColorAsState(target.errorContainer, spec, label = "$label.errorContainer") + val onErrorContainer by animateColorAsState(target.onErrorContainer, spec, label = "$label.onErrorContainer") + val outline by animateColorAsState(target.outline, spec, label = "$label.outline") + val outlineVariant by animateColorAsState(target.outlineVariant, spec, label = "$label.outlineVariant") + val scrim by animateColorAsState(target.scrim, spec, label = "$label.scrim") + val surfaceBright by animateColorAsState(target.surfaceBright, spec, label = "$label.surfaceBright") + val surfaceDim by animateColorAsState(target.surfaceDim, spec, label = "$label.surfaceDim") + val surfaceContainer by animateColorAsState(target.surfaceContainer, spec, label = "$label.surfaceContainer") + val surfaceContainerHigh by animateColorAsState(target.surfaceContainerHigh, spec, label = "$label.surfaceContainerHigh") + val surfaceContainerHighest by animateColorAsState(target.surfaceContainerHighest, spec, label = "$label.surfaceContainerHighest") + val surfaceContainerLow by animateColorAsState(target.surfaceContainerLow, spec, label = "$label.surfaceContainerLow") + val surfaceContainerLowest by animateColorAsState(target.surfaceContainerLowest, spec, label = "$label.surfaceContainerLowest") + + return target.copy( + primary = primary, + onPrimary = onPrimary, + primaryContainer = primaryContainer, + onPrimaryContainer = onPrimaryContainer, + inversePrimary = inversePrimary, + secondary = secondary, + onSecondary = onSecondary, + secondaryContainer = secondaryContainer, + onSecondaryContainer = onSecondaryContainer, + tertiary = tertiary, + onTertiary = onTertiary, + tertiaryContainer = tertiaryContainer, + onTertiaryContainer = onTertiaryContainer, + background = background, + onBackground = onBackground, + surface = surface, + onSurface = onSurface, + surfaceVariant = surfaceVariant, + onSurfaceVariant = onSurfaceVariant, + surfaceTint = surfaceTint, + inverseSurface = inverseSurface, + inverseOnSurface = inverseOnSurface, + error = error, + onError = onError, + errorContainer = errorContainer, + onErrorContainer = onErrorContainer, + outline = outline, + outlineVariant = outlineVariant, + scrim = scrim, + surfaceBright = surfaceBright, + surfaceDim = surfaceDim, + surfaceContainer = surfaceContainer, + surfaceContainerHigh = surfaceContainerHigh, + surfaceContainerHighest = surfaceContainerHighest, + surfaceContainerLow = surfaceContainerLow, + surfaceContainerLowest = surfaceContainerLowest, + ) +} diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index ca591c133..951507a79 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -28,7 +28,8 @@ fun GithubStoreTheme( } val tokenPalette = appTheme.toTokenPalette() val palette = Tokens.palette(tokenPalette, mode) - val scheme = colorSchemeFor(palette = tokenPalette, mode = mode) + val targetScheme = colorSchemeFor(palette = tokenPalette, mode = mode) + val animatedScheme = animateColorScheme(target = targetScheme) CompositionLocalProvider( LocalPalette provides palette, @@ -38,7 +39,7 @@ fun GithubStoreTheme( LocalSpacing provides defaultSpacing, ) { MaterialExpressiveTheme( - colorScheme = scheme, + colorScheme = animatedScheme, typography = getAppTypography(fontTheme), motionScheme = MotionScheme.expressive(), shapes = MaterialTheme.shapes, From b988a7926d7758e5dfbec4a04f9e94cd292fbf44 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:15:01 +0500 Subject: [PATCH 134/172] feat(tweaks): refactor appearance into mode/palette/display sections --- .../composeResources/values/strings.xml | 2 + .../components/sections/Appearance.kt | 530 +++++++++++------- 2 files changed, 321 insertions(+), 211 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 7add4f8ed..7e020ea9b 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1067,6 +1067,8 @@ Library Updates Account + Mode + Display Appearance Language Connection diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index 36c547885..8a9dbcdae 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -1,9 +1,11 @@ package zed.rainxch.tweaks.presentation.components.sections import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke @@ -18,6 +20,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -28,13 +31,9 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.filled.LightMode -import androidx.compose.material.icons.outlined.SettingsBrightness import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -42,17 +41,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.getPlatform import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.Platform +import zed.rainxch.core.presentation.components.hub.GhsSectionHeader import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.theme.tokens.Tokens import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor @@ -63,22 +60,74 @@ import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksState import zed.rainxch.tweaks.presentation.components.ToggleSettingCard +private enum class ModeChoice { LIGHT, DARK, SYSTEM } + +private fun isDarkToChoice(value: Boolean?): ModeChoice = when (value) { + true -> ModeChoice.DARK + false -> ModeChoice.LIGHT + null -> ModeChoice.SYSTEM +} + +private fun choiceToIsDark(choice: ModeChoice): Boolean? = when (choice) { + ModeChoice.DARK -> true + ModeChoice.LIGHT -> false + ModeChoice.SYSTEM -> null +} + +@OptIn(ExperimentalLayoutApi::class) fun LazyListScope.appearanceSection( state: TweaksState, onAction: (TweaksAction) -> Unit, ) { - item { - ThemePickerCard( + item(key = "mode_header") { + GhsSectionHeader(text = stringResource(Res.string.appearance_section_mode)) + Spacer(Modifier.height(8.dp)) + } + item(key = "mode_tiles") { + ModeTiles( + current = isDarkToChoice(state.isDarkTheme), + paletteForPreview = state.selectedThemeColor, + onSelected = { onAction(TweaksAction.OnDarkThemeChange(choiceToIsDark(it))) }, + ) + Spacer(Modifier.height(16.dp)) + } + item(key = "palette_header") { + GhsSectionHeader(text = stringResource(Res.string.theme_color)) + Spacer(Modifier.height(8.dp)) + } + item(key = "palette_grid") { + PaletteGrid( isDarkTheme = state.isDarkTheme, - selectedPalette = state.selectedThemeColor, amoledEnabled = state.isAmoledThemeEnabled, - onDarkThemeChange = { onAction(TweaksAction.OnDarkThemeChange(it)) }, - onPaletteSelected = { onAction(TweaksAction.OnThemeColorSelected(it)) }, - onAmoledToggled = { onAction(TweaksAction.OnAmoledThemeToggled(it)) }, + selected = state.selectedThemeColor, + onSelected = { onAction(TweaksAction.OnThemeColorSelected(it)) }, ) - - Spacer(Modifier.height(12.dp)) - + } + item(key = "amoled_toggle") { + val systemDark = isSystemInDarkTheme() + val resolvedDark = state.isDarkTheme ?: systemDark + AnimatedVisibility( + visible = resolvedDark, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column { + Spacer(Modifier.height(8.dp)) + ToggleSettingCard( + title = stringResource(Res.string.amoled_black_theme), + description = stringResource(Res.string.amoled_black_description), + checked = state.isAmoledThemeEnabled, + onCheckedChange = { onAction(TweaksAction.OnAmoledThemeToggled(it)) }, + ) + } + } + } + item(key = "display_header") { + Spacer(Modifier.height(16.dp)) + GhsSectionHeader(text = stringResource(Res.string.appearance_section_display)) + Spacer(Modifier.height(8.dp)) + } + item(key = "system_font") { ToggleSettingCard( title = stringResource(Res.string.system_font), description = stringResource(Res.string.system_font_description), @@ -91,8 +140,9 @@ fun LazyListScope.appearanceSection( ) }, ) - - if (getPlatform() != Platform.ANDROID) { + } + if (getPlatform() != Platform.ANDROID) { + item(key = "scrollbar") { Spacer(Modifier.height(8.dp)) ToggleSettingCard( title = stringResource(Res.string.scrollbar_option_title), @@ -100,6 +150,8 @@ fun LazyListScope.appearanceSection( checked = state.isScrollbarEnabled, onCheckedChange = { onAction(TweaksAction.OnScrollbarToggled(it)) }, ) + } + item(key = "content_width") { Spacer(Modifier.height(8.dp)) ContentWidthCard( selected = state.contentWidth, @@ -109,163 +161,237 @@ fun LazyListScope.appearanceSection( } } -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun ThemePickerCard( - isDarkTheme: Boolean?, - selectedPalette: AppTheme, - amoledEnabled: Boolean, - onDarkThemeChange: (Boolean?) -> Unit, - onPaletteSelected: (AppTheme) -> Unit, - onAmoledToggled: (Boolean) -> Unit, -) { - val systemDark = isSystemInDarkTheme() - val resolvedDark = isDarkTheme ?: systemDark - val previewMode = when { - resolvedDark && amoledEnabled -> Tokens.Mode.AMOLED - resolvedDark -> Tokens.Mode.DARK - else -> Tokens.Mode.LIGHT - } - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), - ), - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(Res.string.theme_color), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - - Spacer(Modifier.height(12.dp)) - - // Mode segment - ModeSegment( - isDarkTheme = isDarkTheme, - onChange = onDarkThemeChange, - ) - - Spacer(Modifier.height(16.dp)) - - // Palette grid - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - AppTheme.entries.forEach { palette -> - PaletteSwatch( - palette = palette, - mode = previewMode, - selected = palette == selectedPalette, - onClick = { onPaletteSelected(palette) }, - ) - } - } - - AnimatedVisibility( - visible = resolvedDark, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column { - Spacer(Modifier.height(14.dp)) - InlineToggleRow( - title = stringResource(Res.string.amoled_black_theme), - description = stringResource(Res.string.amoled_black_description), - checked = amoledEnabled, - onCheckedChange = onAmoledToggled, - ) - } - } - } - } -} - @Composable -private fun ModeSegment( - isDarkTheme: Boolean?, - onChange: (Boolean?) -> Unit, +private fun ModeTiles( + current: ModeChoice, + paletteForPreview: AppTheme, + onSelected: (ModeChoice) -> Unit, ) { + val token = paletteForPreview.toTokenPalette() + val lightScheme = colorSchemeFor(token, Tokens.Mode.LIGHT) + val darkScheme = colorSchemeFor(token, Tokens.Mode.DARK) Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - SegmentItem( - icon = Icons.Default.LightMode, + ModeTile( label = stringResource(Res.string.theme_light), - selected = isDarkTheme == false, - onClick = { onChange(false) }, + selected = current == ModeChoice.LIGHT, + preview = { ThemePreviewCanvas(scheme = lightScheme) }, + onClick = { onSelected(ModeChoice.LIGHT) }, modifier = Modifier.weight(1f), ) - SegmentItem( - icon = Icons.Default.DarkMode, + ModeTile( label = stringResource(Res.string.theme_dark), - selected = isDarkTheme == true, - onClick = { onChange(true) }, + selected = current == ModeChoice.DARK, + preview = { ThemePreviewCanvas(scheme = darkScheme) }, + onClick = { onSelected(ModeChoice.DARK) }, modifier = Modifier.weight(1f), ) - SegmentItem( - icon = Icons.Outlined.SettingsBrightness, + ModeTile( label = stringResource(Res.string.theme_system), - selected = isDarkTheme == null, - onClick = { onChange(null) }, + selected = current == ModeChoice.SYSTEM, + preview = { + Row(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.weight(1f).fillMaxSize()) { + ThemePreviewCanvas(scheme = lightScheme, edgeFade = true) + } + Box(modifier = Modifier.weight(1f).fillMaxSize()) { + ThemePreviewCanvas(scheme = darkScheme, edgeFade = true) + } + } + }, + onClick = { onSelected(ModeChoice.SYSTEM) }, modifier = Modifier.weight(1f), ) } } @Composable -private fun SegmentItem( - icon: ImageVector, +private fun ModeTile( label: String, selected: Boolean, + preview: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val container = - if (selected) MaterialTheme.colorScheme.primary - else Color.Transparent - val content = - if (selected) MaterialTheme.colorScheme.onPrimary - else MaterialTheme.colorScheme.onSurfaceVariant - Box( + val borderColor by animateColorAsState( + targetValue = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + animationSpec = tween(durationMillis = 220), + label = "mode_border", + ) + val borderWidth by animateFloatAsState( + targetValue = if (selected) 2f else 1f, + animationSpec = tween(durationMillis = 220), + label = "mode_border_w", + ) + Column( modifier = modifier - .height(40.dp) - .clip(RoundedCornerShape(10.dp)) - .background(container) - .clickable(onClick = onClick), - contentAlignment = Alignment.Center, + .clip(Radii.row) + .clickable(onClick = onClick) + .padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = content, - ) - Text( - text = label, - style = MaterialTheme.typography.labelMedium.copy( - fontWeight = FontWeight.SemiBold, + Box( + modifier = Modifier + .fillMaxWidth() + .height(96.dp) + .clip(RoundedCornerShape(16.dp)) + .border( + width = borderWidth.dp, + color = borderColor, + shape = RoundedCornerShape(16.dp), ), - color = content, + contentAlignment = Alignment.Center, + ) { + preview() + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(18.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(12.dp), + ) + } + } + } + Text( + text = label, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + ), + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } +} + +@Composable +private fun ThemePreviewCanvas( + scheme: androidx.compose.material3.ColorScheme, + edgeFade: Boolean = false, +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(scheme.background), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.55f) + .height(6.dp) + .clip(RoundedCornerShape(50)) + .background(scheme.onSurface.copy(alpha = if (edgeFade) 0.55f else 0.85f)), ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(28.dp) + .clip(RoundedCornerShape(8.dp)) + .background(scheme.surfaceContainerHigh) + .padding(6.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(scheme.primary), + ) + Box( + modifier = Modifier + .height(6.dp) + .fillMaxWidth(0.5f) + .clip(RoundedCornerShape(50)) + .background(scheme.onSurfaceVariant.copy(alpha = 0.6f)), + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Box( + modifier = Modifier + .height(18.dp) + .width(44.dp) + .clip(RoundedCornerShape(50)) + .background(scheme.primary), + ) + Box( + modifier = Modifier + .height(18.dp) + .width(28.dp) + .clip(RoundedCornerShape(50)) + .background(scheme.secondaryContainer), + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun PaletteGrid( + isDarkTheme: Boolean?, + amoledEnabled: Boolean, + selected: AppTheme, + onSelected: (AppTheme) -> Unit, +) { + val systemDark = isSystemInDarkTheme() + val resolvedDark = isDarkTheme ?: systemDark + val previewMode = when { + resolvedDark && amoledEnabled -> Tokens.Mode.AMOLED + resolvedDark -> Tokens.Mode.DARK + else -> Tokens.Mode.LIGHT + } + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + AppTheme.entries.forEach { palette -> + PaletteSwatch( + palette = palette, + mode = previewMode, + isSelected = palette == selected, + onClick = { onSelected(palette) }, + ) + } } } } @@ -274,18 +400,32 @@ private fun SegmentItem( private fun PaletteSwatch( palette: AppTheme, mode: Tokens.Mode, - selected: Boolean, + isSelected: Boolean, onClick: () -> Unit, ) { val scheme = colorSchemeFor(palette.toTokenPalette(), mode) val scale by animateFloatAsState( - targetValue = if (selected) 1.0f else 0.98f, + targetValue = if (isSelected) 1.0f else 0.96f, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), label = "swatch_scale", ) + val borderColor by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + animationSpec = tween(durationMillis = 220), + label = "swatch_border", + ) + val borderWidth by animateFloatAsState( + targetValue = if (isSelected) 2f else 1f, + animationSpec = tween(durationMillis = 220), + label = "swatch_border_w", + ) Column( modifier = Modifier - .width(74.dp) + .width(78.dp) .scale(scale) .clip(RoundedCornerShape(16.dp)) .clickable(onClick = onClick), @@ -294,29 +434,24 @@ private fun PaletteSwatch( ) { Box( modifier = Modifier - .size(width = 74.dp, height = 56.dp) + .size(width = 78.dp, height = 60.dp) .clip(RoundedCornerShape(14.dp)) .background(scheme.background) .border( - width = if (selected) 2.dp else 1.dp, - color = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f) - }, + width = borderWidth.dp, + color = borderColor, shape = RoundedCornerShape(14.dp), ), contentAlignment = Alignment.Center, ) { - // Mini palette preview: primary blob + secondary dot Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Box( modifier = Modifier - .size(width = 22.dp, height = 22.dp) - .clip(RoundedCornerShape(7.dp)) + .size(width = 24.dp, height = 24.dp) + .clip(RoundedCornerShape(8.dp)) .background(scheme.primary), ) Box( @@ -332,7 +467,7 @@ private fun PaletteSwatch( .background(scheme.tertiary), ) } - if (selected) { + if (isSelected) { Box( modifier = Modifier .align(Alignment.TopEnd) @@ -354,9 +489,9 @@ private fun PaletteSwatch( Text( text = palette.displayName, style = MaterialTheme.typography.labelSmall.copy( - fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, ), - color = if (selected) { + color = if (isSelected) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.onSurfaceVariant @@ -365,42 +500,6 @@ private fun PaletteSwatch( } } -@Composable -private fun InlineToggleRow( - title: String, - description: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable { onCheckedChange(!checked) } - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = description, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = checked, - onCheckedChange = null, - ) - } -} - @Composable private fun ContentWidthCard( selected: ContentWidth, @@ -410,10 +509,7 @@ private fun ContentWidthCard( modifier = Modifier.fillMaxWidth(), shape = Radii.row, color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), - ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Column(modifier = Modifier.padding(16.dp)) { Text( @@ -435,12 +531,24 @@ private fun ContentWidthCard( ) { ContentWidth.entries.forEach { width -> val isSelected = width == selected - val container = - if (isSelected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.surfaceContainerHigh - val content = - if (isSelected) MaterialTheme.colorScheme.onPrimary - else MaterialTheme.colorScheme.onSurface + val container by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + animationSpec = tween(durationMillis = 220), + label = "cw_container", + ) + val content by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + animationSpec = tween(durationMillis = 220), + label = "cw_content", + ) Box( modifier = Modifier .weight(1f) From a55a7676dbf32cbddd69fc8b249dd83e07b5913f Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:15:01 +0500 Subject: [PATCH 135/172] i18n(appearance): translate Mode/Display to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 2 ++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../src/commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 2 ++ .../src/commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 2 ++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 2 ++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ 12 files changed, 24 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index a194f2105..aef42ade0 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1317,4 +1317,6 @@ المكتبة التحديثات الحساب + الوضع + العرض diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 08ff1d1a1..dd4872c7a 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1279,4 +1279,6 @@ লাইব্রেরি আপডেট অ্যাকাউন্ট + মোড + ডিসপ্লে diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index b1ef2ebeb..ce7efdcac 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1258,4 +1258,6 @@ Biblioteca Actualizaciones Cuenta + Modo + Pantalla diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 3600ef15e..f2743436a 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1258,4 +1258,6 @@ Bibliothèque Mises à jour Compte + Mode + Affichage diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 0a973232a..bba45a0c4 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1294,4 +1294,6 @@ लाइब्रेरी अपडेट अकाउंट + मोड + डिस्प्ले diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index d6ae5de78..5c1b63799 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1292,4 +1292,6 @@ Libreria Aggiornamenti Account + Modalità + Visualizzazione diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 0d7a0d9a0..31471f3f9 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1248,4 +1248,6 @@ ライブラリ アップデート アカウント + モード + 表示 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 5733e2206..28150a11d 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1278,4 +1278,6 @@ 라이브러리 업데이트 계정 + 모드 + 디스플레이 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 913a3fa92..fb887f20c 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1287,4 +1287,6 @@ Biblioteka Aktualizacje Konto + Tryb + Wyświetlanie diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index a5332432e..0b8592b3e 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1287,4 +1287,6 @@ Библиотека Обновления Аккаунт + Режим + Отображение diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 01ec09973..a83ef393f 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1289,4 +1289,6 @@ Kitaplık Güncellemeler Hesap + Mod + Görüntü diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index fdb5f26a1..7f08fe411 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1251,4 +1251,6 @@ 媒体库 更新 账户 + 模式 + 显示 From b90dce569b85ed4c4a54988f61f6656ef9c512c0 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:21:57 +0500 Subject: [PATCH 136/172] feat(tweaks): restyle palette swatch as mini-ui preview --- .../components/sections/Appearance.kt | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index 8a9dbcdae..527ef68b5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -425,47 +425,73 @@ private fun PaletteSwatch( ) Column( modifier = Modifier - .width(78.dp) + .width(82.dp) .scale(scale) - .clip(RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(18.dp)) .clickable(onClick = onClick), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp), ) { Box( modifier = Modifier - .size(width = 78.dp, height = 60.dp) - .clip(RoundedCornerShape(14.dp)) + .size(width = 82.dp, height = 78.dp) + .clip(RoundedCornerShape(16.dp)) .background(scheme.background) .border( width = borderWidth.dp, color = borderColor, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(16.dp), ), - contentAlignment = Alignment.Center, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { + Column(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier - .size(width = 24.dp, height = 24.dp) - .clip(RoundedCornerShape(8.dp)) + .fillMaxWidth() + .height(32.dp) .background(scheme.primary), ) Box( modifier = Modifier - .size(14.dp) - .clip(CircleShape) - .background(scheme.secondary), - ) - Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background(scheme.tertiary), - ) + .fillMaxSize() + .padding(horizontal = 8.dp, vertical = 6.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + Box( + modifier = Modifier + .height(4.dp) + .fillMaxWidth(0.75f) + .clip(RoundedCornerShape(50)) + .background(scheme.onSurface.copy(alpha = 0.55f)), + ) + Box( + modifier = Modifier + .height(4.dp) + .fillMaxWidth(0.45f) + .clip(RoundedCornerShape(50)) + .background(scheme.onSurface.copy(alpha = 0.32f)), + ) + Spacer(Modifier.height(1.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(scheme.secondary), + ) + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(scheme.tertiary), + ) + } + } + } } if (isSelected) { Box( From f9f4b3f1a109b1cbea708c41da090b0aa2cfd0ee Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:26:36 +0500 Subject: [PATCH 137/172] feat(theme): add dynamic color palette for Android 12+ --- .../zed/rainxch/core/domain/model/AppTheme.kt | 2 +- .../theme/DynamicColorScheme.android.kt | 18 ++++++++++++ .../composeResources/values/strings.xml | 1 + .../presentation/theme/DynamicColorScheme.kt | 9 ++++++ .../rainxch/core/presentation/theme/Theme.kt | 22 ++++++++++++++- .../core/presentation/theme/tokens/Tokens.kt | 7 ++++- .../core/presentation/utils/AppThemeUtil.kt | 3 ++ .../theme/DynamicColorScheme.jvm.kt | 9 ++++++ .../components/sections/Appearance.kt | 28 +++++++++++++------ 9 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.android.kt create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.kt create mode 100644 core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.jvm.kt diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt index 69b34d578..444fd73ab 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/AppTheme.kt @@ -1,6 +1,7 @@ package zed.rainxch.core.domain.model enum class AppTheme { + DYNAMIC, NORD, CREAM, FOREST, @@ -10,7 +11,6 @@ enum class AppTheme { companion object { private val LEGACY_MIGRATION = mapOf( - "DYNAMIC" to NORD, "OCEAN" to NORD, "SLATE" to NORD, "PURPLE" to PLUM, diff --git a/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.android.kt b/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.android.kt new file mode 100644 index 000000000..7e2b0ff81 --- /dev/null +++ b/core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.android.kt @@ -0,0 +1,18 @@ +package zed.rainxch.core.presentation.theme + +import android.os.Build +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +actual fun isDynamicColorAvailable(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + +@Composable +actual fun dynamicColorScheme(isDark: Boolean): ColorScheme? { + if (!isDynamicColorAvailable()) return null + val context = LocalContext.current + return if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) +} diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 7e020ea9b..4c4571290 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -152,6 +152,7 @@ Forest + Dynamic Nord Cream Plum diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.kt new file mode 100644 index 000000000..b2ab32838 --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.kt @@ -0,0 +1,9 @@ +package zed.rainxch.core.presentation.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +expect fun isDynamicColorAvailable(): Boolean + +@Composable +expect fun dynamicColorScheme(isDark: Boolean): ColorScheme? diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index 951507a79..76d1dac75 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -1,11 +1,13 @@ package zed.rainxch.core.presentation.theme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.presentation.theme.tokens.Tokens @@ -28,7 +30,25 @@ fun GithubStoreTheme( } val tokenPalette = appTheme.toTokenPalette() val palette = Tokens.palette(tokenPalette, mode) - val targetScheme = colorSchemeFor(palette = tokenPalette, mode = mode) + val dynamic = if (appTheme == AppTheme.DYNAMIC) { + dynamicColorScheme(isDark = isDarkTheme) + } else { + null + } + val baseScheme = colorSchemeFor(palette = tokenPalette, mode = mode) + val targetScheme = if (dynamic != null) { + if (isAmoledTheme && isDarkTheme) { + dynamic.copy( + background = Color.Black, + surface = Color(0xFF0B0F14), + surfaceContainerLowest = Color.Black, + ) + } else { + dynamic + } + } else { + baseScheme + } val animatedScheme = animateColorScheme(target = targetScheme) CompositionLocalProvider( diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt index 7f837d26c..2c92db714 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/tokens/Tokens.kt @@ -3,7 +3,7 @@ package zed.rainxch.core.presentation.theme.tokens import androidx.compose.ui.graphics.Color object Tokens { - enum class Palette { NORD, CREAM, FOREST, PLUM } + enum class Palette { DYNAMIC, NORD, CREAM, FOREST, PLUM } enum class Mode { LIGHT, DARK, AMOLED } @@ -167,6 +167,11 @@ object Tokens { } fun palette(p: Palette, m: Mode): PaletteColors = when (p) { + Palette.DYNAMIC -> when (m) { + Mode.LIGHT -> Nord.light + Mode.DARK -> Nord.dark + Mode.AMOLED -> Nord.amoled + } Palette.NORD -> when (m) { Mode.LIGHT -> Nord.light Mode.DARK -> Nord.dark diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt index cd7c179cc..8b450ea84 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/AppThemeUtil.kt @@ -9,11 +9,13 @@ import zed.rainxch.core.presentation.theme.tokens.Tokens import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.theme_cream +import zed.rainxch.githubstore.core.presentation.res.theme_dynamic import zed.rainxch.githubstore.core.presentation.res.theme_forest import zed.rainxch.githubstore.core.presentation.res.theme_nord import zed.rainxch.githubstore.core.presentation.res.theme_plum fun AppTheme.toTokenPalette(): Tokens.Palette = when (this) { + AppTheme.DYNAMIC -> Tokens.Palette.DYNAMIC AppTheme.NORD -> Tokens.Palette.NORD AppTheme.CREAM -> Tokens.Palette.CREAM AppTheme.FOREST -> Tokens.Palette.FOREST @@ -36,6 +38,7 @@ val AppTheme.displayName: String @Composable get() = stringResource( when (this) { + AppTheme.DYNAMIC -> Res.string.theme_dynamic AppTheme.NORD -> Res.string.theme_nord AppTheme.CREAM -> Res.string.theme_cream AppTheme.FOREST -> Res.string.theme_forest diff --git a/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.jvm.kt b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.jvm.kt new file mode 100644 index 000000000..b3aa09b4e --- /dev/null +++ b/core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/theme/DynamicColorScheme.jvm.kt @@ -0,0 +1,9 @@ +package zed.rainxch.core.presentation.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +actual fun isDynamicColorAvailable(): Boolean = false + +@Composable +actual fun dynamicColorScheme(isDark: Boolean): ColorScheme? = null diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt index 527ef68b5..c751138a3 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Appearance.kt @@ -50,6 +50,8 @@ import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.components.hub.GhsSectionHeader +import zed.rainxch.core.presentation.theme.dynamicColorScheme +import zed.rainxch.core.presentation.theme.isDynamicColorAvailable import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.theme.tokens.Tokens import zed.rainxch.core.presentation.theme.tokens.colorSchemeFor @@ -384,14 +386,17 @@ private fun PaletteGrid( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - AppTheme.entries.forEach { palette -> - PaletteSwatch( - palette = palette, - mode = previewMode, - isSelected = palette == selected, - onClick = { onSelected(palette) }, - ) - } + val dynamicAvailable = isDynamicColorAvailable() + AppTheme.entries + .filter { it != AppTheme.DYNAMIC || dynamicAvailable } + .forEach { palette -> + PaletteSwatch( + palette = palette, + mode = previewMode, + isSelected = palette == selected, + onClick = { onSelected(palette) }, + ) + } } } } @@ -403,7 +408,12 @@ private fun PaletteSwatch( isSelected: Boolean, onClick: () -> Unit, ) { - val scheme = colorSchemeFor(palette.toTokenPalette(), mode) + val isDark = mode != Tokens.Mode.LIGHT + val scheme = if (palette == AppTheme.DYNAMIC) { + dynamicColorScheme(isDark = isDark) ?: colorSchemeFor(Tokens.Palette.NORD, mode) + } else { + colorSchemeFor(palette.toTokenPalette(), mode) + } val scale by animateFloatAsState( targetValue = if (isSelected) 1.0f else 0.96f, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), From 17bfbb6f1cc272f5a4da97c38e5a4763429d6122 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:26:41 +0500 Subject: [PATCH 138/172] i18n(theme): translate Dynamic palette name to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 1 + .../src/commonMain/composeResources/values-bn/strings-bn.xml | 1 + .../src/commonMain/composeResources/values-es/strings-es.xml | 1 + .../src/commonMain/composeResources/values-fr/strings-fr.xml | 1 + .../src/commonMain/composeResources/values-hi/strings-hi.xml | 1 + .../src/commonMain/composeResources/values-it/strings-it.xml | 1 + .../src/commonMain/composeResources/values-ja/strings-ja.xml | 1 + .../src/commonMain/composeResources/values-ko/strings-ko.xml | 1 + .../src/commonMain/composeResources/values-pl/strings-pl.xml | 1 + .../src/commonMain/composeResources/values-ru/strings-ru.xml | 1 + .../src/commonMain/composeResources/values-tr/strings-tr.xml | 1 + .../commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml | 1 + 12 files changed, 12 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index aef42ade0..aebd1fd20 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1319,4 +1319,5 @@ الحساب الوضع العرض + ديناميكي diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index dd4872c7a..9404f849e 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1281,4 +1281,5 @@ অ্যাকাউন্ট মোড ডিসপ্লে + ডাইনামিক diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index ce7efdcac..38d66530a 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1260,4 +1260,5 @@ Cuenta Modo Pantalla + Dinámico diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index f2743436a..cfa329982 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1260,4 +1260,5 @@ Compte Mode Affichage + Dynamique diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index bba45a0c4..07aae4703 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1296,4 +1296,5 @@ अकाउंट मोड डिस्प्ले + डायनामिक diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 5c1b63799..d5cb8d407 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1294,4 +1294,5 @@ Account Modalità Visualizzazione + Dinamico diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 31471f3f9..17e0f6b0f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1250,4 +1250,5 @@ アカウント モード 表示 + ダイナミック diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 28150a11d..4329b8036 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1280,4 +1280,5 @@ 계정 모드 디스플레이 + 다이내믹 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index fb887f20c..2d4749494 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1289,4 +1289,5 @@ Konto Tryb Wyświetlanie + Dynamiczny diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 0b8592b3e..e25298a01 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1289,4 +1289,5 @@ Аккаунт Режим Отображение + Динамичная diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index a83ef393f..6a7053439 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1291,4 +1291,5 @@ Hesap Mod Görüntü + Dinamik diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 7f08fe411..61e7dcf5b 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1253,4 +1253,5 @@ 账户 模式 显示 + 动态 From bd189a4c66783f257ddc5df3d264fc47aa91aeb6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 15:40:53 +0500 Subject: [PATCH 139/172] feat(desktop): cap content width on wide windows --- .../app/onboarding/OnboardingScreen.kt | 76 +++++++++++-------- .../utils/ConstrainedContentWidth.kt | 20 +++++ .../zed/rainxch/apps/presentation/AppsRoot.kt | 9 ++- .../auth/presentation/AuthenticationRoot.kt | 11 ++- .../zed/rainxch/home/presentation/HomeRoot.kt | 8 +- .../profile/presentation/ProfileRoot.kt | 41 ++++++---- .../rainxch/search/presentation/SearchRoot.kt | 11 ++- .../rainxch/tweaks/presentation/TweaksRoot.kt | 11 ++- .../components/TweaksSubScreenScaffold.kt | 25 ++++-- 9 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ConstrainedContentWidth.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt index fcc9f6058..b2bc27801 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/onboarding/OnboardingScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -46,8 +47,10 @@ import zed.rainxch.core.domain.model.ThemeMode import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import zed.rainxch.core.presentation.theme.geist +import zed.rainxch.core.presentation.theme.isDynamicColorAvailable import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.core.presentation.utils.primaryColor import zed.rainxch.core.presentation.vocabulary.CookieShape import zed.rainxch.core.presentation.vocabulary.Squiggle @@ -73,39 +76,47 @@ fun OnboardingScreen( state: OnboardingState, onAction: (OnboardingAction) -> Unit, ) { - Column( + Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) - .systemBarsPadding() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .systemBarsPadding(), + contentAlignment = Alignment.TopCenter, ) { - StepIndicator(total = state.steps.size, currentIndex = state.currentIndex) - Spacer(Modifier.height(32.dp)) + Column( + modifier = + Modifier + .constrainedContentWidth() + .fillMaxHeight() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StepIndicator(total = state.steps.size, currentIndex = state.currentIndex) + Spacer(Modifier.height(32.dp)) - val permissionsController = rememberOnboardingPermissionsController() + val permissionsController = rememberOnboardingPermissionsController() - AnimatedContent( - targetState = state.currentStep, - transitionSpec = { - ( - slideInHorizontally { it } + fadeIn() togetherWith - slideOutHorizontally { -it } + fadeOut() - ) - }, - label = "onboarding-step", - modifier = Modifier.weight(1f).fillMaxWidth(), - ) { step -> - when (step) { - OnboardingStep.PALETTE -> StepPalette(state, onAction) - OnboardingStep.SIGN_IN -> StepSignIn(onAction) - OnboardingStep.PERMISSIONS -> StepPermissions(permissionsController) + AnimatedContent( + targetState = state.currentStep, + transitionSpec = { + ( + slideInHorizontally { it } + fadeIn() togetherWith + slideOutHorizontally { -it } + fadeOut() + ) + }, + label = "onboarding-step", + modifier = Modifier.weight(1f).fillMaxWidth(), + ) { step -> + when (step) { + OnboardingStep.PALETTE -> StepPalette(state, onAction) + OnboardingStep.SIGN_IN -> StepSignIn(onAction) + OnboardingStep.PERMISSIONS -> StepPermissions(permissionsController) + } } - } - ActionRow(state, onAction) + ActionRow(state, onAction) + } } } @@ -161,13 +172,16 @@ private fun StepPalette( textAlign = TextAlign.Center, ) Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - AppTheme.entries.forEach { palette -> - PaletteSwatch( - palette = palette, - isSelected = state.selectedPalette == palette, - onClick = { onAction(OnboardingAction.OnPaletteSelected(palette)) }, - ) - } + val dynamicAvailable = isDynamicColorAvailable() + AppTheme.entries + .filter { it != AppTheme.DYNAMIC || dynamicAvailable } + .forEach { palette -> + PaletteSwatch( + palette = palette, + isSelected = state.selectedPalette == palette, + onClick = { onAction(OnboardingAction.OnPaletteSelected(palette)) }, + ) + } } Spacer(Modifier.height(16.dp)) ModeRow( diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ConstrainedContentWidth.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ConstrainedContentWidth.kt new file mode 100644 index 000000000..8e9ca5b6a --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/ConstrainedContentWidth.kt @@ -0,0 +1,20 @@ +package zed.rainxch.core.presentation.utils + +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import zed.rainxch.core.domain.model.ContentWidth +import zed.rainxch.core.presentation.locals.LocalContentWidth + +@Composable +@ReadOnlyComposable +fun Modifier.constrainedContentWidth(): Modifier { + val cap = when (LocalContentWidth.current) { + ContentWidth.COMPACT -> 680.dp + ContentWidth.WIDE -> 960.dp + ContentWidth.EXTRA_WIDE -> null + } + return if (cap != null) widthIn(max = cap) else this +} diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index a01ae5f76..ee9186dd1 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -104,6 +105,7 @@ import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.apps.presentation.model.UpdateState import zed.rainxch.core.presentation.components.ExpressiveCard import zed.rainxch.core.presentation.components.chrome.GhsHomeTopBar +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.core.presentation.components.ScrollbarContainer import zed.rainxch.core.presentation.components.buttons.GhsButton import zed.rainxch.core.presentation.components.buttons.GhsButtonSize @@ -535,8 +537,12 @@ fun AppsScreen( .fillMaxSize() .padding(innerPadding), ) { - Column( + Box( modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + Column( + modifier = Modifier.constrainedContentWidth().fillMaxHeight(), ) { GhsTextField( value = state.searchQuery, @@ -896,6 +902,7 @@ fun AppsScreen( } } } + } } } } diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index 12daf9a74..d73b71b68 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -95,6 +96,7 @@ import zed.rainxch.core.presentation.components.inputs.passwordVisualTransformat import zed.rainxch.core.presentation.components.overlays.GhsBottomSheet import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.auth_use_device_code_instead import zed.rainxch.githubstore.core.presentation.res.app_icon @@ -156,10 +158,14 @@ fun AuthenticationScreen( modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.TopCenter, + ) { Column( modifier = Modifier - .fillMaxSize() - .padding(innerPadding) + .constrainedContentWidth() + .fillMaxHeight() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -234,6 +240,7 @@ fun AuthenticationScreen( onAction = onAction, ) } + } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 3cb29ba73..ba546f0b0 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -39,6 +39,7 @@ import zed.rainxch.core.presentation.components.section.SectionHeader import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.home_finding_repositories import zed.rainxch.githubstore.core.presentation.res.home_retry @@ -120,7 +121,11 @@ private fun HomeScreen( }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> - Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.TopCenter, + ) { + Box(modifier = Modifier.constrainedContentWidth().fillMaxSize()) { val sectionsAreEmpty = state.lead == null && state.hot.isEmpty() && state.trending.isEmpty() && state.popular.isEmpty() && state.starred.isEmpty() val isAnyLoading = state.isHotLoading || state.isTrendingLoading || @@ -320,5 +325,6 @@ private fun HomeScreen( ) } } + } } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 74ba6f663..0d03f1692 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -1,6 +1,8 @@ package zed.rainxch.profile.presentation +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -18,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -31,6 +34,7 @@ import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.downloads_cleared import zed.rainxch.githubstore.core.presentation.res.logout_success @@ -195,23 +199,28 @@ fun ProfileScreen( containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> val listState = rememberLazyListState() - LazyColumn( - state = listState, - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(16.dp) - .arrowKeyScroll(listState, autoFocus = true), + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.TopCenter, ) { - profileSections( - state = state, - hasUnreadAnnouncements = hasUnreadAnnouncements, - onAction = onAction, - ) - - item { - Spacer(Modifier.height(bottomNavHeight + 32.dp)) + LazyColumn( + state = listState, + modifier = + Modifier + .constrainedContentWidth() + .fillMaxHeight() + .padding(16.dp) + .arrowKeyScroll(listState, autoFocus = true), + ) { + profileSections( + state = state, + hasUnreadAnnouncements = hasUnreadAnnouncements, + onAction = onAction, + ) + + item { + Spacer(Modifier.height(bottomNavHeight + 32.dp)) + } } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 48ae51a4b..f6b7efc53 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -96,6 +97,7 @@ import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.search.presentation.components.LanguageFilterBottomSheet import zed.rainxch.search.presentation.components.SearchHistorySection @@ -351,11 +353,15 @@ fun SearchScreen( }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.TopCenter, + ) { Column( modifier = Modifier - .fillMaxSize() - .padding(innerPadding) + .constrainedContentWidth() + .fillMaxHeight() .padding(horizontal = 16.dp), ) { @@ -618,6 +624,7 @@ fun SearchScreen( } } } + } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 351a4b692..f0d2c7e80 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -2,6 +2,7 @@ package zed.rainxch.tweaks.presentation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -61,6 +62,7 @@ import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.theme.GithubStoreTheme import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.core.presentation.utils.arrowKeyScroll +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.components.RestartBanner import zed.rainxch.core.presentation.components.hub.GhsEntryRow @@ -367,11 +369,15 @@ fun TweaksHubScreen( containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> val listState = rememberLazyListState() + Box( + modifier = Modifier.fillMaxSize().padding(innerPadding), + contentAlignment = Alignment.TopCenter, + ) { LazyColumn( state = listState, modifier = Modifier - .fillMaxSize() - .padding(innerPadding) + .constrainedContentWidth() + .fillMaxHeight() .padding(horizontal = 16.dp) .arrowKeyScroll(listState, autoFocus = true), ) { @@ -439,6 +445,7 @@ fun TweaksHubScreen( Spacer(Modifier.height(bottomNavHeight + 32.dp)) } } + } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt index 888b99085..5b3822876 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/TweaksSubScreenScaffold.kt @@ -1,7 +1,9 @@ package zed.rainxch.tweaks.presentation.components +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -20,6 +22,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -27,6 +30,7 @@ import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.domain.model.RestartReason import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight import zed.rainxch.core.presentation.utils.arrowKeyScroll +import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.back_cd @@ -75,15 +79,21 @@ fun TweaksSubScreenScaffold( containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> val listState = rememberLazyListState() - LazyColumn( - state = listState, + Box( modifier = Modifier .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 16.dp) - .arrowKeyScroll(listState, autoFocus = true), - contentPadding = PaddingValues(top = 8.dp, bottom = bottomNavHeight + 32.dp), + .padding(innerPadding), + contentAlignment = Alignment.TopCenter, ) { + LazyColumn( + state = listState, + modifier = Modifier + .constrainedContentWidth() + .fillMaxHeight() + .padding(horizontal = 16.dp) + .arrowKeyScroll(listState, autoFocus = true), + contentPadding = PaddingValues(top = 8.dp, bottom = bottomNavHeight + 32.dp), + ) { if (showRestartBanner && restartReasons.isNotEmpty()) { item(key = "restart_banner") { RestartBanner( @@ -94,7 +104,8 @@ fun TweaksSubScreenScaffold( Spacer(Modifier.height(16.dp)) } } - content() + content() + } } } } From 88d4d6475d2f9c69afa487a351b95bb36042a9c4 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:09:53 +0500 Subject: [PATCH 140/172] fix(proxy): auto-save system/none, optimistic use-main toggle --- .../tweaks/presentation/TweaksViewModel.kt | 38 +++++++++++++++++++ .../connection/TweaksConnectionRoot.kt | 27 ------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index f3a164588..11d9bbd18 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -1194,6 +1194,39 @@ class TweaksViewModel( is TweaksAction.OnMasterProxyTypeSelected -> { mutateMasterForm { it.copy(type = action.type) } + + if (action.type == ProxyType.NONE || action.type == ProxyType.SYSTEM) { + val config = + if (action.type == ProxyType.NONE) { + ProxyConfig.None + } else { + ProxyConfig.System + } + viewModelScope.launch { + runCatching { + proxyRepository.setMasterProxyConfig(config) + ProxyScope.entries.forEach { scope -> + if (_state.value.useMain(scope)) { + proxyRepository.setProxyConfig(scope, config) + } + } + }.onSuccess { + _state.update { + it.copy( + masterProxyForm = it.masterProxyForm.copy(isDraftDirty = false), + ) + } + _events.send(TweaksEvent.OnProxySaved) + }.onFailure { error -> + _events.send( + TweaksEvent.OnProxySaveError( + error.message + ?: getString(Res.string.failed_to_save_proxy_settings), + ), + ) + } + } + } } is TweaksAction.OnMasterProxyHostChanged -> { @@ -1327,6 +1360,11 @@ class TweaksViewModel( } is TweaksAction.OnScopeUseMainToggled -> { + _state.update { + it.copy( + useMasterByScope = it.useMasterByScope + (action.scope to action.useMain), + ) + } viewModelScope.launch { runCatching { proxyRepository.setUseMaster(action.scope, action.useMain) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt index b87afec9c..2bd308958 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/connection/TweaksConnectionRoot.kt @@ -285,18 +285,6 @@ private fun MainConnectionCard( } } - AnimatedVisibility(visible = form.type == ProxyType.NONE || form.type == ProxyType.SYSTEM) { - Column { - Spacer(Modifier.height(12.dp)) - GhsButton( - onClick = { onAction(TweaksAction.OnMasterProxySave) }, - label = stringResource(Res.string.proxy_save), - variant = GhsButtonVariant.Primary, - leadingIcon = Icons.Default.Save, - modifier = Modifier.fillMaxWidth(), - ) - } - } } } } @@ -567,21 +555,6 @@ private fun ScopeOverrideRow( } } - AnimatedVisibility( - visible = scopeForm.type == ProxyType.NONE || - scopeForm.type == ProxyType.SYSTEM, - ) { - Column { - Spacer(Modifier.height(12.dp)) - GhsButton( - onClick = { onAction(TweaksAction.OnProxySave(scope)) }, - label = stringResource(Res.string.proxy_save), - variant = GhsButtonVariant.Tonal, - leadingIcon = Icons.Default.Save, - modifier = Modifier.fillMaxWidth(), - ) - } - } } } } From 6711b9e7c2c38e63a955ec25861a1e67d7de14ea Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:09:55 +0500 Subject: [PATCH 141/172] fix(tweaks): hide installs/updates section on desktop --- .../rainxch/tweaks/presentation/TweaksRoot.kt | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index f0d2c7e80..4d1b316cd 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -65,6 +65,8 @@ import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.core.presentation.utils.constrainedContentWidth import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.tweaks.presentation.components.RestartBanner +import zed.rainxch.core.domain.getPlatform +import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.components.hub.GhsEntryRow import zed.rainxch.core.presentation.components.hub.GhsSectionHeader import zed.rainxch.core.presentation.theme.tokens.GhsAccents @@ -208,8 +210,9 @@ fun TweaksHubScreen( var query by rememberSaveable { mutableStateOf("") } val tapToManage = stringResource(Res.string.tweaks_entry_subtitle_tap) + val isAndroid = getPlatform() == Platform.ANDROID - val blocks = listOf( + val blocks = listOfNotNull( TweaksHubBlock( title = stringResource(Res.string.section_look_and_feel), entries = listOf( @@ -255,25 +258,29 @@ fun TweaksHubScreen( ), ), ), - TweaksHubBlock( - title = stringResource(Res.string.section_installs_and_updates), - entries = listOf( - TweaksHubEntry( - title = stringResource(Res.string.tweaks_entry_install_method), - subtitle = tapToManage, - icon = Icons.Outlined.InstallMobile, - onClick = onNavigateToInstallMethod, - accent = GhsAccents.Sage, - ), - TweaksHubEntry( - title = stringResource(Res.string.tweaks_entry_updates), - subtitle = tapToManage, - icon = Icons.Outlined.Update, - onClick = onNavigateToUpdates, - accent = GhsAccents.Amber, + if (isAndroid) { + TweaksHubBlock( + title = stringResource(Res.string.section_installs_and_updates), + entries = listOf( + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_install_method), + subtitle = tapToManage, + icon = Icons.Outlined.InstallMobile, + onClick = onNavigateToInstallMethod, + accent = GhsAccents.Sage, + ), + TweaksHubEntry( + title = stringResource(Res.string.tweaks_entry_updates), + subtitle = tapToManage, + icon = Icons.Outlined.Update, + onClick = onNavigateToUpdates, + accent = GhsAccents.Amber, + ), ), - ), - ), + ) + } else { + null + }, TweaksHubBlock( title = stringResource(Res.string.section_privacy_and_data), entries = listOf( From fc7fdb532945f32d966f674e14944c7627885642 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:16:28 +0500 Subject: [PATCH 142/172] fix(profile): logout dialog uses GhsConfirmDialog --- .../components/overlays/GhsConfirmDialog.kt | 9 ++ .../presentation/components/LogoutDialog.kt | 97 ++++--------------- 2 files changed, 26 insertions(+), 80 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt index e13480561..6b88bb4f7 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/overlays/GhsConfirmDialog.kt @@ -35,6 +35,7 @@ fun GhsConfirmDialog( cancelLabel: String = "Cancel", destructive: Boolean = false, leading: (@Composable () -> Unit)? = null, + note: String? = null, ) { Dialog(onDismissRequest = onDismiss) { val cs = MaterialTheme.colorScheme @@ -65,6 +66,14 @@ fun GhsConfirmDialog( style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, ) + if (!note.isNullOrBlank()) { + Text( + text = note, + color = cs.outline, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + } Squiggle() Row( modifier = Modifier.fillMaxWidth().padding(top = 8.dp), diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt index 2dbf9ce24..04a407c3c 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/LogoutDialog.kt @@ -1,91 +1,28 @@ package zed.rainxch.profile.presentation.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import zed.rainxch.core.presentation.components.buttons.GhsButton -import zed.rainxch.core.presentation.components.buttons.GhsButtonSize -import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties import org.jetbrains.compose.resources.stringResource -import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.core.presentation.components.overlays.GhsConfirmDialog +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.close +import zed.rainxch.githubstore.core.presentation.res.logout +import zed.rainxch.githubstore.core.presentation.res.logout_confirmation +import zed.rainxch.githubstore.core.presentation.res.logout_revocation_note +import zed.rainxch.githubstore.core.presentation.res.warning -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogoutDialog( onDismissRequest: () -> Unit, onLogout: () -> Unit, - modifier: Modifier = Modifier, ) { - BasicAlertDialog( - onDismissRequest = onDismissRequest, - properties = - DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - ), - modifier = - modifier - .padding(16.dp) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(16.dp), - ) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(Res.string.warning), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - ) - - Text( - text = stringResource(Res.string.logout_confirmation), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Text( - text = stringResource(Res.string.logout_revocation_note), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), - ) { - GhsButton( - onClick = { onDismissRequest() }, - label = stringResource(Res.string.close), - variant = GhsButtonVariant.Text, - size = GhsButtonSize.Sm, - ) - - GhsButton( - onClick = onLogout, - label = stringResource(Res.string.logout), - variant = GhsButtonVariant.Destructive, - size = GhsButtonSize.Sm, - ) - } - } - } + GhsConfirmDialog( + title = stringResource(Res.string.warning), + body = stringResource(Res.string.logout_confirmation), + note = stringResource(Res.string.logout_revocation_note), + confirmLabel = stringResource(Res.string.logout), + cancelLabel = stringResource(Res.string.close), + destructive = true, + onConfirm = onLogout, + onDismiss = onDismissRequest, + ) } From d4ad23f41de0066388b4fa2ec9ba88fae35a2c34 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:22:05 +0500 Subject: [PATCH 143/172] l10n: cleanup single quote escaping across multiple locales --- .../composeResources/values-ar/strings-ar.xml | 6 +- .../composeResources/values-bn/strings-bn.xml | 4 +- .../composeResources/values-es/strings-es.xml | 4 +- .../composeResources/values-fr/strings-fr.xml | 90 +++++++++---------- .../composeResources/values-hi/strings-hi.xml | 4 +- .../composeResources/values-it/strings-it.xml | 6 +- .../composeResources/values-ko/strings-ko.xml | 4 +- .../composeResources/values-pl/strings-pl.xml | 4 +- .../composeResources/values-tr/strings-tr.xml | 22 ++--- .../values-zh-rCN/strings-zh-rCN.xml | 2 +- .../composeResources/values/strings.xml | 26 +++--- 11 files changed, 86 insertions(+), 86 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index aebd1fd20..d3a4fa531 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1219,7 +1219,7 @@ ما الجديد يُطبَّق على كل الحركة ما لم يُعَد تجاوزه أدناه. مخصّص - اختر وضع الاتصال أدناه. معظم المستخدمين يتركونه على \'بدون وكيل\'. + اختر وضع الاتصال أدناه. معظم المستخدمين يتركونه على 'بدون وكيل'. كيف يصل التطبيق إلى الإنترنت الاتصال الرئيسي HTTP/HTTPS @@ -1228,7 +1228,7 @@ SOCKS5 Tor، أنفاق SSH. النظام - يستخدم كل نطاق الاتصال الرئيسي افتراضياً. اختر \'مخصّص\' للاحتفاظ بإعداداته الخاصة. + يستخدم كل نطاق الاتصال الرئيسي افتراضياً. اختر 'مخصّص' للاحتفاظ بإعداداته الخاصة. تجاوزات لكل نطاق لصق رابط كامل الصق رابط وكيل كاملاً وسنملأ النموذج نيابةً عنك. @@ -1285,7 +1285,7 @@ مشاركة بيانات استخدام مجهولة ما الذي نجمعه بيانات الاستخدام - لا توجد إعدادات تطابق \'%1$s\'. + لا توجد إعدادات تطابق '%1$s'. البحث في الإعدادات إضافة مضيف Forgejo أو Gitea المضيفات المضافة diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 9404f849e..702628146 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1198,7 +1198,7 @@ SOCKS5 Tor, SSH টানেল। সিস্টেম - প্রতিটি স্কোপ ডিফল্টভাবে প্রধান সংযোগ ব্যবহার করে। নিজস্ব সেটিংস রাখতে \'কাস্টম\' বেছে নিন। + প্রতিটি স্কোপ ডিফল্টভাবে প্রধান সংযোগ ব্যবহার করে। নিজস্ব সেটিংস রাখতে 'কাস্টম' বেছে নিন। প্রতি-স্কোপ ওভাররাইড সম্পূর্ণ URL পেস্ট করুন একটি সম্পূর্ণ প্রক্সি URL পেস্ট করুন, আমরা আপনার জন্য ফর্ম পূরণ করব। @@ -1255,7 +1255,7 @@ বেনামী ব্যবহারের তথ্য শেয়ার করুন আমরা কী সংগ্রহ করি ব্যবহারের তথ্য - \'%1$s\' এর সাথে কোনো সেটিংস মেলে না। + '%1$s' এর সাথে কোনো সেটিংস মেলে না। সেটিংস অনুসন্ধান করুন একটি Forgejo বা Gitea হোস্ট যোগ করুন যোগ করা হোস্ট diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 38d66530a..de063750d 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1173,7 +1173,7 @@ SOCKS5 Tor, túneles SSH. Sistema - Cada ámbito usa la conexión principal por defecto. Elige \'Personalizado\' para mantener sus propios ajustes. + Cada ámbito usa la conexión principal por defecto. Elige 'Personalizado' para mantener sus propios ajustes. Sustituciones por ámbito Pegar URL completa Pega una URL de proxy completa y rellenaremos el formulario por ti. @@ -1230,7 +1230,7 @@ Compartir datos de uso anónimos Qué recopilamos Datos de uso - Ningún ajuste coincide con \'%1$s\'. + Ningún ajuste coincide con '%1$s'. Buscar ajustes Añadir un host de Forgejo o Gitea Hosts añadidos diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index cfa329982..a0e5c366d 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -477,7 +477,7 @@ Barre de défilement Afficher la barre de défilement dans les listes défilantes (bureau) Largeur du contenu - Largeur maximale de la colonne sur l\'écran de détails (bureau) + Largeur maximale de la colonne sur l'écran de détails (bureau) Compact Large Très large @@ -1012,7 +1012,7 @@ Choisir la variante Masquer la liste Voir la liste - Examinez ou mettez tout à jour d\'un coup. + Examinez ou mettez tout à jour d'un coup. 1 mise à jour disponible %1$d mises à jour disponibles Retour @@ -1058,15 +1058,15 @@ Touchez + pour ajouter un Personal Access Token Aucun jeton enregistré Personal access tokens par hôte de forge (GitHub, Codeberg, Forgejo) - Nom d\'affichage (facultatif) + Nom d'affichage (facultatif) Personal access token - Vous êtes connecté à GitHub via l\'application — c\'est la méthode recommandée. N\'ajoutez un jeton ici que si la connexion par navigateur ne fonctionne pas (réseau restrictif, pas de navigateur) ou pour des forges autres que GitHub. + Vous êtes connecté à GitHub via l'application — c'est la méthode recommandée. N'ajoutez un jeton ici que si la connexion par navigateur ne fonctionne pas (réseau restrictif, pas de navigateur) ou pour des forges autres que GitHub. Ouvrir la page des jetons Autre forge Forgejo, Gitea, auto-hébergé Coller le jeton Ajouter un jeton pour… - Modifier l\'étiquette + Modifier l'étiquette Invalide · %1$s Plus Gérer sur %1$s @@ -1075,7 +1075,7 @@ Valide · %1$s Vérifier Jeton enregistré pour %1$s - Jetons d\'authentification + Jetons d'authentification Annuler Impossible de restaurer le jeton pour %1$s Jeton pour %1$s supprimé @@ -1091,10 +1091,10 @@ Aide Politique de confidentialité Paramètres, thème, réseau, traduction, options avancées. - Certains changements nécessitent un redémarrage pour s\'appliquer. + Certains changements nécessitent un redémarrage pour s'appliquer. Plus tard langue - données d\'usage + données d'usage thème Concerné : %1$s Redémarrer @@ -1116,36 +1116,36 @@ %1$d app %1$d apps - Traduit automatiquement le README et les notes de version à l\'ouverture d\'un dépôt. Utilise la langue de l\'app comme cible. - Selon la langue de l\'app (%1$s) + Traduit automatiquement le README et les notes de version à l'ouverture d'un dépôt. Utilise la langue de l'app comme cible. + Selon la langue de l'app (%1$s) Traduire en Traduire les dépôts automatiquement Annuler Changer de langue Source détectée : %1$s Réessayer - Afficher l\'original + Afficher l'original Afficher la traduction Affichez cette page dans une autre langue. - Affichage de l\'original + Affichage de l'original Traduit en %1$s Traduction… Langue cible Traduire Traduire en %1$s - Clé d\'authentification + Clé d'authentification Obtenir une clé DeepL gratuite → - Inscrivez-vous sur deepl.com/pro-api. Les clés du niveau gratuit se terminent par :fx et utilisent automatiquement l\'endpoint gratuit. Le niveau Pro préserve la confidentialité de votre texte ; le niveau gratuit peut utiliser les textes soumis pour améliorer les modèles. + Inscrivez-vous sur deepl.com/pro-api. Les clés du niveau gratuit se terminent par :fx et utilisent automatiquement l'endpoint gratuit. Le niveau Pro préserve la confidentialité de votre texte ; le niveau gratuit peut utiliser les textes soumis pour améliorer les modèles. Enregistrer la clé Clé DeepL enregistrée Clé API (facultative) - URL de l\'instance - Fonctionne d\'emblée via le miroir public Disroot — laissez les champs vides pour l\'utiliser. Pour plus de confidentialité, collez l\'URL de votre instance auto-hébergée. Une clé API n\'est nécessaire que si l\'instance l\'exige. + URL de l'instance + Fonctionne d'emblée via le miroir public Disroot — laissez les champs vides pour l'utiliser. Pour plus de confidentialité, collez l'URL de votre instance auto-hébergée. Une clé API n'est nécessaire que si l'instance l'exige. Enregistrer Paramètres LibreTranslate enregistrés Obtenir une clé Azure gratuite → - Azure Translator respecte la vie privée (No-Trace par défaut — votre texte n\'est jamais stocké ni utilisé pour l\'entraînement). Le niveau gratuit couvre 2 millions de caractères par mois. La région n\'est nécessaire que pour les ressources non globales. - Clé d\'abonnement + Azure Translator respecte la vie privée (No-Trace par défaut — votre texte n'est jamais stocké ni utilisé pour l'entraînement). Le niveau gratuit couvre 2 millions de caractères par mois. La région n'est nécessaire que pour les ressources non globales. + Clé d'abonnement Région (ex. westeurope, laissez vide pour Global) Enregistrer les identifiants Identifiants Microsoft Translator enregistrés @@ -1153,48 +1153,48 @@ LibreTranslate Microsoft GitHub Store - Bibliothèques utilisées par l\'application. + Bibliothèques utilisées par l'application. Licences open source Consulter sur github-store.org. Politique de confidentialité - Consulter le code source de l\'application. + Consulter le code source de l'application. Code source sur GitHub - Boutique d\'applications multi-plateforme pour les versions GitHub, Codeberg et Forgejo. + Boutique d'applications multi-plateforme pour les versions GitHub, Codeberg et Forgejo. Notes des versions précédentes. Nouveautés - S\'applique à tout le trafic sauf si remplacé ci-dessous. + S'applique à tout le trafic sauf si remplacé ci-dessous. Personnalisé Choisissez un mode de connexion ci-dessous. La plupart des utilisateurs gardent « Aucun proxy ». - Comment l\'application accède à internet + Comment l'application accède à internet Connexion principale HTTP/HTTPS - La plupart des proxys d\'entreprise. + La plupart des proxys d'entreprise. Aucun proxy SOCKS5 Tor, tunnels SSH. Système Chaque portée utilise la connexion principale par défaut. Choisissez « Personnalisé » pour conserver des paramètres dédiés. Remplacements par portée - Coller l\'URL complète + Coller l'URL complète Collez une URL de proxy complète et nous remplirons le formulaire pour vous. Utiliser cette URL Impossible de lire cette URL. scheme://user:pass@host:port - Coller l\'URL du proxy + Coller l'URL du proxy API GitHub, recherche, détails du dépôt. Recherche & métadonnées - Téléchargements d\'APK et d\'assets. + Téléchargements d'APK et d'assets. Téléchargements Appels DeepL, Microsoft, LibreTranslate. Traduction Tester Utiliser la principale - Jetons d\'accès - Infos de l\'app + Jetons d'accès + Infos de l'app Apparence Connexion Envoyer un commentaire - Méthode d\'installation + Méthode d'installation Langue Confidentialité Sources @@ -1202,48 +1202,48 @@ Toucher pour gérer Traduction Comportement des mises à jour - Les installateurs silencieux et l\'attribution d\'installeur sont des fonctionnalités Android. Sur ordinateur, les installations passent par le gestionnaire de paquets du système. - La méthode d\'installation est réservée à Android - L\'application redémarre lors du changement de langue. + Les installateurs silencieux et l'attribution d'installeur sont des fonctionnalités Android. Sur ordinateur, les installations passent par le gestionnaire de paquets du système. + La méthode d'installation est réservée à Android + L'application redémarre lors du changement de langue. Touchez une entrée pour ouvrir la page du projet. - GitHub Store s\'appuie sur ces bibliothèques. + GitHub Store s'appuie sur ces bibliothèques. Licences open source Historique de navigation Oublier quels dépôts vous avez déjà ouverts. Cela ne retire pas vos étoiles ni vos favoris. Effacer - Effacer l\'historique des consultations ? - Effacer l\'historique des consultations - Lorsque vous copiez un lien github.com ou codeberg.org, nous vous proposons de l\'ouvrir. + Effacer l'historique des consultations ? + Effacer l'historique des consultations + Lorsque vous copiez un lien github.com ou codeberg.org, nous vous proposons de l'ouvrir. Détecter les liens de dépôts dans le presse-papiers Dépôts que vous avez masqués des flux et de la recherche. Dépôts masqués Ignorer les dépôts déjà vus dans les flux et la recherche. Masquer les dépôts déjà consultés Aidez-nous à comprendre quelles fonctionnalités sont utilisées. - Version de l\'application. - Nombre d\'utilisations des fonctionnalités. + Version de l'application. + Nombre d'utilisations des fonctionnalités. Aucun identifiant. Aucun nom de dépôt. Aucun jeton. - Système d\'exploitation et plateforme. - Partager des données d\'usage anonymes + Système d'exploitation et plateforme. + Partager des données d'usage anonymes Ce que nous collectons - Données d\'usage + Données d'usage Aucun paramètre ne correspond à « %1$s ». Rechercher dans les paramètres Ajouter un hôte Forgejo ou Gitea Hôtes ajoutés Miroir GitHub - L\'application interroge GitHub par défaut. Passez par un miroir régional ou ajoutez des hôtes Forgejo / Gitea personnalisés. - Où l\'application cherche les dépôts + L'application interroge GitHub par défaut. Passez par un miroir régional ou ajoutez des hôtes Forgejo / Gitea personnalisés. + Où l'application cherche les dépôts Par défaut (github.com) Nous conservons les installateurs pour que les mises à jour reprennent rapidement. Vider APK téléchargés 0 o Utilisation : %1$s - Cet écran fait partie de la refonte en cours. Les paramètres fonctionnent toujours — ils sont simplement en cours de migration depuis l\'ancienne disposition. + Cet écran fait partie de la refonte en cours. Les paramètres fonctionnent toujours — ils sont simplement en cours de migration depuis l'ancienne disposition. Bientôt disponible %1$d dépôt masqué diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 07aae4703..337e6f5a7 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1196,7 +1196,7 @@ SOCKS5 Tor, SSH टनल। सिस्टम - प्रत्येक स्कोप डिफ़ॉल्ट रूप से मुख्य कनेक्शन का उपयोग करता है। अपनी सेटिंग्स रखने के लिए \'कस्टम\' चुनें। + प्रत्येक स्कोप डिफ़ॉल्ट रूप से मुख्य कनेक्शन का उपयोग करता है। अपनी सेटिंग्स रखने के लिए 'कस्टम' चुनें। प्रति-स्कोप ओवरराइड पूरा URL पेस्ट करें एक पूरा प्रॉक्सी URL पेस्ट करें और हम आपके लिए फ़ॉर्म भर देंगे। @@ -1253,7 +1253,7 @@ अनाम उपयोग डेटा साझा करें हम क्या एकत्र करते हैं उपयोग डेटा - कोई सेटिंग \'%1$s\' से मेल नहीं खाती। + कोई सेटिंग '%1$s' से मेल नहीं खाती। सेटिंग्स खोजें Forgejo या Gitea होस्ट जोड़ें जोड़े गए होस्ट diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index d5cb8d407..59ede43c3 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1207,12 +1207,12 @@ SOCKS5 Tor, tunnel SSH. Sistema - Ogni ambito usa la connessione principale per impostazione predefinita. Scegli \'Personalizzato\' per mantenerne uno proprio. + Ogni ambito usa la connessione principale per impostazione predefinita. Scegli 'Personalizzato' per mantenerne uno proprio. Override per ambito Incolla URL completo Incolla un URL proxy completo e compileremo noi il modulo. Usa questo URL - Impossibile leggere quell\'URL. + Impossibile leggere quell'URL. scheme://user:pass@host:port Incolla URL del proxy API di GitHub, ricerca, dettagli repo. @@ -1264,7 +1264,7 @@ Condividi dati di utilizzo anonimi Cosa raccogliamo Dati di utilizzo - Nessuna impostazione corrisponde a \'%1$s\'. + Nessuna impostazione corrisponde a '%1$s'. Cerca impostazioni Aggiungi un host Forgejo o Gitea Host aggiunti diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 4329b8036..fac130f8c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1194,7 +1194,7 @@ SOCKS5 Tor, SSH 터널. 시스템 - 각 스코프는 기본적으로 주 연결을 사용합니다. 자체 설정을 유지하려면 \'사용자 지정\'을 선택하세요. + 각 스코프는 기본적으로 주 연결을 사용합니다. 자체 설정을 유지하려면 '사용자 지정'을 선택하세요. 스코프별 재정의 전체 URL 붙여넣기 전체 프록시 URL을 붙여넣으면 양식을 자동으로 채워 드립니다. @@ -1251,7 +1251,7 @@ 익명 사용 데이터 공유 수집 항목 사용 데이터 - \'%1$s\'와(과) 일치하는 설정이 없습니다. + '%1$s'와(과) 일치하는 설정이 없습니다. 설정 검색 Forgejo 또는 Gitea 호스트 추가 추가된 호스트 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 2d4749494..0cbd486c0 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1200,7 +1200,7 @@ SOCKS5 Tor, tunele SSH. Systemowe - Każdy zakres domyślnie używa głównego połączenia. Wybierz \'Niestandardowe\', aby zachować własne ustawienia. + Każdy zakres domyślnie używa głównego połączenia. Wybierz 'Niestandardowe', aby zachować własne ustawienia. Nadpisania per-zakres Wklej pełny URL Wklej pełny URL proxy, a wypełnimy formularz za Ciebie. @@ -1257,7 +1257,7 @@ Udostępniaj anonimowe dane użycia Co zbieramy Dane użycia - Żadne ustawienie nie pasuje do \'%1$s\'. + Żadne ustawienie nie pasuje do '%1$s'. Szukaj ustawień Dodaj host Forgejo lub Gitea Dodane hosty diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 6a7053439..31b9e875c 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1099,7 +1099,7 @@ Her forge hostu için personal access token (GitHub, Codeberg, Forgejo) Görünen ad (isteğe bağlı) Personal access token - Uygulamadan GitHub\'a giriş yaptınız — bu önerilen yoldur. Buraya yalnızca tarayıcı girişi çalışmıyorsa (kısıtlı ağ, tarayıcı yok) veya GitHub dışındaki forgeler için token ekleyin. + Uygulamadan GitHub'a giriş yaptınız — bu önerilen yoldur. Buraya yalnızca tarayıcı girişi çalışmıyorsa (kısıtlı ağ, tarayıcı yok) veya GitHub dışındaki forgeler için token ekleyin. Token sayfasını aç Diğer forge Forgejo, Gitea, self-hosted @@ -1174,8 +1174,8 @@ Anahtarı kaydet DeepL anahtarı kaydedildi API anahtarı (isteğe bağlı) - Sunucu URL\'si - Public Disroot aynası aracılığıyla kutudan çıkar çıkmaz çalışır — kullanmak için alanları boş bırakın. Daha fazla gizlilik için kendi self-hosted URL\'nizi yapıştırın. API anahtarı yalnızca sunucu istiyorsa gereklidir. + Sunucu URL'si + Public Disroot aynası aracılığıyla kutudan çıkar çıkmaz çalışır — kullanmak için alanları boş bırakın. Daha fazla gizlilik için kendi self-hosted URL'nizi yapıştırın. API anahtarı yalnızca sunucu istiyorsa gereklidir. Ayarları kaydet LibreTranslate ayarları kaydedildi Ücretsiz Azure anahtarı alın → @@ -1193,7 +1193,7 @@ github-store.org üzerinde gör. Gizlilik politikası Bu uygulamanın kaynağını gör. - GitHub\'da kaynak kodu + GitHub'da kaynak kodu GitHub, Codeberg ve Forgejo sürümleri için çapraz platform uygulama mağazası. Geçmiş sürüm notları. Neler yeni @@ -1208,14 +1208,14 @@ SOCKS5 Tor, SSH tünelleri. Sistem - Her kapsam varsayılan olarak ana bağlantıyı kullanır. Kendi ayarlarını korumak için \'Özel\'i seçin. + Her kapsam varsayılan olarak ana bağlantıyı kullanır. Kendi ayarlarını korumak için 'Özel'i seçin. Kapsama göre geçersiz kılmalar Tam URL yapıştır - Tam bir proxy URL\'si yapıştırın, formu sizin için dolduralım. - Bu URL\'yi kullan + Tam bir proxy URL'si yapıştırın, formu sizin için dolduralım. + Bu URL'yi kullan Bu URL okunamadı. scheme://user:pass@host:port - Proxy URL\'sini yapıştır + Proxy URL'sini yapıştır GitHub API, arama, repo ayrıntıları. Arama ve meta veri APK ve dosya indirmeleri. @@ -1265,17 +1265,17 @@ Anonim kullanım verisi paylaş Ne topluyoruz Kullanım verisi - \'%1$s\' ile eşleşen ayar yok. + '%1$s' ile eşleşen ayar yok. Ayarları ara Bir Forgejo veya Gitea hostu ekle Eklenen hostlar GitHub aynası - Uygulama varsayılan olarak GitHub\'da arama yapar. Bölgesel bir aynaya yönlendirin veya özel Forgejo / Gitea hostları ekleyin. + Uygulama varsayılan olarak GitHub'da arama yapar. Bölgesel bir aynaya yönlendirin veya özel Forgejo / Gitea hostları ekleyin. Uygulama repoları nerede arar Varsayılan (github.com) Güncellemelerin hızlı devam etmesi için yükleyicileri saklarız. Temizle - İndirilen APK\'lar + İndirilen APK'lar 0 B Kullanılıyor: %1$s Bu ekran sürmekte olan yeniden tasarımın bir parçası. Ayarlar hâlâ çalışıyor — sadece eski düzenden buraya taşınıyor. diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 61e7dcf5b..145dcd4e2 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1224,7 +1224,7 @@ 共享匿名使用数据 我们收集的内容 使用数据 - 没有设置匹配 \'%1$s\'。 + 没有设置匹配 '%1$s'。 搜索设置 添加 Forgejo 或 Gitea 主机 已添加的主机 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 4c4571290..18b72aead 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -579,7 +579,7 @@ Check every How often to look for new releases. Check automatically - Look for new releases on a schedule. Turn off to save battery — you can still check manually from any app\'s details screen. + Look for new releases on a schedule. Turn off to save battery — you can still check manually from any app's details screen. Every 3 hours Every 6 hours Every 12 hours @@ -1084,7 +1084,7 @@ Send feedback Tap to manage Search settings - No settings match \'%1$s\'. + No settings match '%1$s'. We read every report. Some changes need a restart to apply. Affected: %1$s @@ -1096,7 +1096,7 @@ Install method is Android-only Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager. Coming in a follow-up - This screen is part of the in-progress redesign. The settings still work — they\'re just being moved here from the old layout. + This screen is part of the in-progress redesign. The settings still work — they're just being moved here from the old layout. Back Help About GitHub Store @@ -1244,13 +1244,13 @@ Pick a connection mode below. Most people leave this on No proxy. Main connection Per-scope overrides - Each scope uses the main connection by default. Choose \'Custom\' to keep its own settings. + Each scope uses the main connection by default. Choose 'Custom' to keep its own settings. Applies to all traffic unless overridden below. Paste full URL Paste proxy URL - Paste a full proxy URL and we\'ll fill the form for you. + Paste a full proxy URL and we'll fill the form for you. scheme://user:pass@host:port - Couldn\'t read that URL. + Couldn't read that URL. Use this URL Search & metadata GitHub API, search, repo details. @@ -1287,16 +1287,16 @@ No tokens. No identifiers. Detect repo links in clipboard - When you copy a github.com or codeberg.org link, we\'ll prompt to open it. - Hide repos I\'ve already viewed + When you copy a github.com or codeberg.org link, we'll prompt to open it. + Hide repos I've already viewed Skip seen repos in feeds and search. Clear viewed history - Forget which repos you\'ve already opened. + Forget which repos you've already opened. Clear viewed history? - This won\'t unstar or unfavorite anything. + This won't unstar or unfavorite anything. Clear Hidden repositories - Repos you\'ve muted from feeds and search. + Repos you've muted from feeds and search. Downloaded APKs We keep installers around so updates resume fast. @@ -1312,14 +1312,14 @@ Business inquiries Partnerships, press, integrations. Contact - What\'s new + What's new Past release notes. Open source licenses Libraries used in the app. Privacy policy View on github-store.org. Source code on GitHub - View this app\'s source. + View this app's source. The app restarts when you switch language. From 47e2f09cc7b9ab836ecddccb0d75817373a07b6c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:23:18 +0500 Subject: [PATCH 144/172] build: use sequences in printRuntimeDependencies task + clean up types --- composeApp/build.gradle.kts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index baf533828..61f7cc1d5 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -171,23 +171,24 @@ compose.desktop { } } -// Maintainer helper: prints sorted unique runtime dependencies of the Android release variant. -// Use to audit the curated licenses.json after adding/removing deps. -// ./gradlew :composeApp:printRuntimeDependencies tasks.register("printRuntimeDependencies") { description = "Lists all runtime dependencies for licenses.json maintenance." group = "verification" - val coordinatesProvider: Provider> = project.provider { - val cfg = configurations.findByName("releaseRuntimeClasspath") - ?: configurations.findByName("androidReleaseRuntimeClasspath") - ?: error("No release runtime classpath configuration found.") - cfg.incoming.resolutionResult.allComponents - .map { it.id } - .filterIsInstance() - .map { "${it.group}:${it.module}:${it.version}" } - .distinct() - .sorted() - } + val coordinatesProvider: Provider> = + project.provider { + val cfg = + configurations.findByName("releaseRuntimeClasspath") + ?: configurations.findByName("androidReleaseRuntimeClasspath") + ?: error("No release runtime classpath configuration found.") + cfg.incoming.resolutionResult.allComponents + .asSequence() + .map { it.id } + .filterIsInstance() + .map { "${it.group}:${it.module}:${it.version}" } + .distinct() + .sorted() + .toList() + } notCompatibleWithConfigurationCache("Resolves Android variant configurations at execution time.") doLast { coordinatesProvider.get().forEach { println(it) } From d5c2fb03acfec4ad1d114700c6aa604fc35a2e15 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:32:59 +0500 Subject: [PATCH 145/172] feat: remove telemetry completely --- .../data/repository/TweaksRepositoryImpl.kt | 9 -- .../core/domain/model/RestartReason.kt | 1 - .../domain/repository/TweaksRepository.kt | 4 - .../composeResources/values-ar/strings-ar.xml | 17 +-- .../composeResources/values-bn/strings-bn.xml | 17 +-- .../composeResources/values-es/strings-es.xml | 17 +-- .../composeResources/values-fr/strings-fr.xml | 17 +-- .../composeResources/values-hi/strings-hi.xml | 17 +-- .../composeResources/values-it/strings-it.xml | 17 +-- .../composeResources/values-ja/strings-ja.xml | 17 +-- .../composeResources/values-ko/strings-ko.xml | 17 +-- .../composeResources/values-pl/strings-pl.xml | 17 +-- .../composeResources/values-ru/strings-ru.xml | 17 +-- .../composeResources/values-tr/strings-tr.xml | 17 +-- .../values-zh-rCN/strings-zh-rCN.xml | 17 +-- .../composeResources/values/strings.xml | 16 +-- .../tweaks/presentation/TweaksAction.kt | 2 - .../tweaks/presentation/TweaksState.kt | 2 - .../tweaks/presentation/TweaksViewModel.kt | 24 ---- .../presentation/components/RestartBanner.kt | 2 - .../presentation/privacy/TweaksPrivacyRoot.kt | 123 ------------------ 21 files changed, 26 insertions(+), 361 deletions(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index c679756bb..67d418322 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -488,14 +488,6 @@ class TweaksRepositoryImpl( } } - override fun getTelemetryEnabled(): Flow = - gatedGetFlow(K_TELEMETRY_ENABLED, false) - - override suspend fun setTelemetryEnabled(enabled: Boolean) { - migrationDeferred.await() - ksafe.safePut(K_TELEMETRY_ENABLED, enabled) - } - companion object { private const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L private const val MIGRATION_MARKER = "__migrated_from_datastore_v1__" @@ -545,6 +537,5 @@ class TweaksRepositoryImpl( private const val K_CONTENT_WIDTH = "content_width" private const val K_CUSTOM_FORGE_HOSTS = "custom_forge_hosts" private const val K_RESTART_REASONS = "needs_restart_reasons" - private const val K_TELEMETRY_ENABLED = "telemetry_enabled" } } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt index 61d1ceef5..5a57645bb 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RestartReason.kt @@ -3,5 +3,4 @@ package zed.rainxch.core.domain.model enum class RestartReason { LANGUAGE, THEME_MIGRATION, - TELEMETRY_TOGGLE, } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index 2ef4cbda2..d984b24ae 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -199,8 +199,4 @@ interface TweaksRepository { suspend fun addRestartReason(reason: RestartReason) suspend fun clearRestartReasons() - - fun getTelemetryEnabled(): Flow - - suspend fun setTelemetryEnabled(enabled: Boolean) } diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index d3a4fa531..059c6ad27 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1144,9 +1144,7 @@ إعدادات التطبيق، السمة، الشبكة، الترجمة، الخيارات المتقدمة. تتطلب بعض التغييرات إعادة تشغيل التطبيق لتفعيلها. لاحقاً - اللغة - بيانات الاستخدام - السمة + اللغة السمة المتأثر: %1$s إعادة التشغيل الآن إزالة الفلتر @@ -1274,18 +1272,7 @@ المستودعات التي كتمتها من الموجَزات والبحث. المستودعات المخفية تخطّي المستودعات المُشاهَدة في الموجَزات والبحث. - إخفاء المستودعات التي شاهدتها بالفعل - ساعدنا في فهم الميزات التي يتم استخدامها. - إصدار التطبيق. - عدد مرات استخدام الميزات. - لا معرّفات. - لا أسماء مستودعات. - لا رموز. - نظام التشغيل والمنصّة. - مشاركة بيانات استخدام مجهولة - ما الذي نجمعه - بيانات الاستخدام - لا توجد إعدادات تطابق '%1$s'. + إخفاء المستودعات التي شاهدتها بالفعل لا معرّفات. نظام التشغيل والمنصّة. لا توجد إعدادات تطابق '%1$s'. البحث في الإعدادات إضافة مضيف Forgejo أو Gitea المضيفات المضافة diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 702628146..8a6a18ddc 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1122,9 +1122,7 @@ অ্যাপ সেটিংস, থিম, নেটওয়ার্ক, অনুবাদ, উন্নত বিকল্প। কিছু পরিবর্তন প্রয়োগ করতে পুনরায় চালু করা প্রয়োজন। পরে - ভাষা - ব্যবহারের তথ্য - থিম + ভাষা থিম প্রভাবিত: %1$s এখন পুনরায় চালু করুন ফিল্টার সরান @@ -1244,18 +1242,7 @@ যেসব রিপো আপনি ফিড ও সার্চ থেকে মিউট করেছেন। লুকানো রিপোজিটরি ফিড ও সার্চে দেখা রিপোগুলি বাদ দিন। - যে রিপো ইতিমধ্যে দেখেছি সেগুলি লুকান - কোন ফিচারগুলি ব্যবহৃত হচ্ছে তা বুঝতে আমাদের সাহায্য করুন। - অ্যাপ সংস্করণ। - ফিচার ব্যবহারের গণনা। - কোনো শনাক্তকারী নেই। - রিপোর নাম নেই। - কোনো টোকেন নেই। - OS এবং প্ল্যাটফর্ম। - বেনামী ব্যবহারের তথ্য শেয়ার করুন - আমরা কী সংগ্রহ করি - ব্যবহারের তথ্য - '%1$s' এর সাথে কোনো সেটিংস মেলে না। + যে রিপো ইতিমধ্যে দেখেছি সেগুলি লুকান কোনো শনাক্তকারী নেই। OS এবং প্ল্যাটফর্ম। '%1$s' এর সাথে কোনো সেটিংস মেলে না। সেটিংস অনুসন্ধান করুন একটি Forgejo বা Gitea হোস্ট যোগ করুন যোগ করা হোস্ট diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index de063750d..0e69e9f44 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1093,9 +1093,7 @@ Ajustes de la app, tema, red, traducción, opciones avanzadas. Algunos cambios necesitan reiniciar para aplicarse. Más tarde - idioma - datos de uso - tema + idioma tema Afectado: %1$s Reiniciar ahora Quitar filtro @@ -1219,18 +1217,7 @@ Repos que has silenciado en los feeds y la búsqueda. Repositorios ocultos Omite los repos vistos en feeds y búsqueda. - Ocultar repos que ya he visto - Ayúdanos a entender qué funciones se usan. - Versión de la app. - Recuentos de uso de funciones. - Sin identificadores. - Sin nombres de repos. - Sin tokens. - Sistema operativo y plataforma. - Compartir datos de uso anónimos - Qué recopilamos - Datos de uso - Ningún ajuste coincide con '%1$s'. + Ocultar repos que ya he visto Sin identificadores. Sistema operativo y plataforma. Ningún ajuste coincide con '%1$s'. Buscar ajustes Añadir un host de Forgejo o Gitea Hosts añadidos diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index a0e5c366d..7c3cc8f28 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1093,9 +1093,7 @@ Paramètres, thème, réseau, traduction, options avancées. Certains changements nécessitent un redémarrage pour s'appliquer. Plus tard - langue - données d'usage - thème + langue thème Concerné : %1$s Redémarrer Retirer le filtre @@ -1219,18 +1217,7 @@ Dépôts que vous avez masqués des flux et de la recherche. Dépôts masqués Ignorer les dépôts déjà vus dans les flux et la recherche. - Masquer les dépôts déjà consultés - Aidez-nous à comprendre quelles fonctionnalités sont utilisées. - Version de l'application. - Nombre d'utilisations des fonctionnalités. - Aucun identifiant. - Aucun nom de dépôt. - Aucun jeton. - Système d'exploitation et plateforme. - Partager des données d'usage anonymes - Ce que nous collectons - Données d'usage - Aucun paramètre ne correspond à « %1$s ». + Masquer les dépôts déjà consultés Aucun identifiant. Système d'exploitation et plateforme. Aucun paramètre ne correspond à « %1$s ». Rechercher dans les paramètres Ajouter un hôte Forgejo ou Gitea Hôtes ajoutés diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 337e6f5a7..7ca264690 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1120,9 +1120,7 @@ ऐप सेटिंग्स, थीम, नेटवर्क, अनुवाद, उन्नत विकल्प। कुछ बदलाव लागू करने के लिए पुनरारंभ की आवश्यकता है। बाद में - भाषा - उपयोग डेटा - थीम + भाषा थीम प्रभावित: %1$s अभी पुनरारंभ करें फ़िल्टर हटाएँ @@ -1242,18 +1240,7 @@ वे रिपो जिन्हें आपने फ़ीड और खोज से म्यूट किया है। छिपी हुई रिपॉजिटरी फ़ीड और खोज में देखी हुई रिपो छोड़ें। - मेरे द्वारा पहले से देखी गई रिपो छिपाएँ - हमें यह समझने में मदद करें कि कौन सी सुविधाओं का उपयोग किया जाता है। - ऐप संस्करण। - सुविधा उपयोग गणना। - कोई पहचानकर्ता नहीं। - कोई रिपो नाम नहीं। - कोई टोकन नहीं। - OS और प्लेटफ़ॉर्म। - अनाम उपयोग डेटा साझा करें - हम क्या एकत्र करते हैं - उपयोग डेटा - कोई सेटिंग '%1$s' से मेल नहीं खाती। + मेरे द्वारा पहले से देखी गई रिपो छिपाएँ कोई पहचानकर्ता नहीं। OS और प्लेटफ़ॉर्म। कोई सेटिंग '%1$s' से मेल नहीं खाती। सेटिंग्स खोजें Forgejo या Gitea होस्ट जोड़ें जोड़े गए होस्ट diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 59ede43c3..4da9b28ea 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1127,9 +1127,7 @@ Impostazioni app, tema, rete, traduzione, opzioni avanzate. Alcune modifiche richiedono un riavvio per essere applicate. Più tardi - lingua - dati di utilizzo - tema + lingua tema Interessati: %1$s Riavvia ora Rimuovi filtro @@ -1253,18 +1251,7 @@ Repo che hai silenziato da feed e ricerca. Repository nascosti Salta i repo già visti in feed e ricerca. - Nascondi repo già visualizzati - Aiutaci a capire quali funzionalità vengono usate. - Versione dell'app. - Conteggi di utilizzo delle funzionalità. - Nessun identificatore. - Nessun nome di repo. - Nessun token. - Sistema operativo e piattaforma. - Condividi dati di utilizzo anonimi - Cosa raccogliamo - Dati di utilizzo - Nessuna impostazione corrisponde a '%1$s'. + Nascondi repo già visualizzati Nessun identificatore. Sistema operativo e piattaforma. Nessuna impostazione corrisponde a '%1$s'. Cerca impostazioni Aggiungi un host Forgejo o Gitea Host aggiunti diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 17e0f6b0f..fd1063060 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1085,9 +1085,7 @@ アプリ設定、テーマ、ネットワーク、翻訳、詳細オプション。 一部の変更を反映するには再起動が必要です。 後で - 言語 - 使用状況データ - テーマ + 言語 テーマ 対象: %1$s 今すぐ再起動 フィルターを解除 @@ -1210,18 +1208,7 @@ フィードと検索からミュートしたリポジトリ。 非表示のリポジトリ 既読のリポジトリをフィードと検索から除外します。 - 既読のリポジトリを隠す - どの機能が使われているかを把握する手助けをしてください。 - アプリのバージョン。 - 機能の使用回数。 - 識別子は収集しません。 - リポジトリ名は収集しません。 - トークンは収集しません。 - OS とプラットフォーム。 - 匿名の使用状況データを共有 - 収集する内容 - 使用状況データ - 「%1$s」に一致する設定はありません。 + 既読のリポジトリを隠す 識別子は収集しません。 OS とプラットフォーム。 「%1$s」に一致する設定はありません。 設定を検索 Forgejo または Gitea ホストを追加 追加したホスト diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index fac130f8c..8dc792daa 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1115,9 +1115,7 @@ 앱 설정, 테마, 네트워크, 번역, 고급 옵션. 일부 변경 사항을 적용하려면 다시 시작해야 합니다. 나중에 - 언어 - 사용 데이터 - 테마 + 언어 테마 영향: %1$s 지금 다시 시작 필터 제거 @@ -1240,18 +1238,7 @@ 피드와 검색에서 숨긴 저장소입니다. 숨긴 저장소 피드와 검색에서 본 저장소를 건너뜁니다. - 이미 본 저장소 숨기기 - 어떤 기능이 사용되는지 파악하는 데 도움을 주세요. - 앱 버전. - 기능 사용 횟수. - 식별자 없음. - 저장소 이름 없음. - 토큰 없음. - OS 및 플랫폼. - 익명 사용 데이터 공유 - 수집 항목 - 사용 데이터 - '%1$s'와(과) 일치하는 설정이 없습니다. + 이미 본 저장소 숨기기 식별자 없음. OS 및 플랫폼. '%1$s'와(과) 일치하는 설정이 없습니다. 설정 검색 Forgejo 또는 Gitea 호스트 추가 추가된 호스트 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 0cbd486c0..945e013fc 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1118,9 +1118,7 @@ Ustawienia aplikacji, motyw, sieć, tłumaczenie, opcje zaawansowane. Niektóre zmiany wymagają ponownego uruchomienia, aby zostały zastosowane. Później - język - dane użycia - motyw + język motyw Dotyczy: %1$s Uruchom ponownie teraz Usuń filtr @@ -1246,18 +1244,7 @@ Repozytoria wyciszone w kanałach i wyszukiwarce. Ukryte repozytoria Pomijaj obejrzane repo w kanałach i wynikach wyszukiwania. - Ukrywaj repo, które już widziałem - Pomóż nam zrozumieć, z których funkcji się korzysta. - Wersja aplikacji. - Liczniki użycia funkcji. - Bez identyfikatorów. - Bez nazw repozytoriów. - Bez tokenów. - System i platforma. - Udostępniaj anonimowe dane użycia - Co zbieramy - Dane użycia - Żadne ustawienie nie pasuje do '%1$s'. + Ukrywaj repo, które już widziałem Bez identyfikatorów. System i platforma. Żadne ustawienie nie pasuje do '%1$s'. Szukaj ustawień Dodaj host Forgejo lub Gitea Dodane hosty diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index e25298a01..01eb54f1c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1118,9 +1118,7 @@ Настройки приложения, тема, сеть, перевод, расширенные параметры. Некоторым изменениям нужен перезапуск. Позже - язык - данные использования - тема + язык тема Затронуто: %1$s Перезапустить Убрать фильтр @@ -1246,18 +1244,7 @@ Репо, которые ты убрал из ленты и поиска. Скрытые репозитории Пропускать просмотренные репо в ленте и поиске. - Скрывать уже просмотренные репо - Помоги нам понять, какие функции используются. - Версия приложения. - Счётчики использования функций. - Никаких идентификаторов. - Никаких названий репо. - Никаких токенов. - ОС и платформа. - Делиться анонимными данными использования - Что мы собираем - Данные использования - Под «%1$s» ничего не нашлось. + Скрывать уже просмотренные репо Никаких идентификаторов. ОС и платформа. Под «%1$s» ничего не нашлось. Поиск по настройкам Добавить хост Forgejo или Gitea Добавленные хосты diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 31b9e875c..970e7f6e6 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1132,9 +1132,7 @@ Uygulama ayarları, tema, ağ, çeviri, gelişmiş seçenekler. Bazı değişikliklerin uygulanması için yeniden başlatma gerekir. Sonra - dil - kullanım verisi - tema + dil tema Etkilenen: %1$s Şimdi yeniden başlat Filtreyi kaldır @@ -1254,18 +1252,7 @@ Akışlardan ve aramadan sessize aldığınız repolar. Gizli depolar Akışlarda ve aramada görülen repoları atla. - Daha önce görüntülediğim repoları gizle - Hangi özelliklerin kullanıldığını anlamamıza yardımcı ol. - Uygulama sürümü. - Özellik kullanım sayıları. - Tanımlayıcı yok. - Repo adı yok. - Token yok. - İşletim sistemi ve platform. - Anonim kullanım verisi paylaş - Ne topluyoruz - Kullanım verisi - '%1$s' ile eşleşen ayar yok. + Daha önce görüntülediğim repoları gizle Tanımlayıcı yok. İşletim sistemi ve platform. '%1$s' ile eşleşen ayar yok. Ayarları ara Bir Forgejo veya Gitea hostu ekle Eklenen hostlar diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 145dcd4e2..8806a574f 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1088,9 +1088,7 @@ 应用设置、主题、网络、翻译、高级选项。 部分更改需要重启后生效。 稍后 - 语言 - 使用数据 - 主题 + 语言 主题 受影响:%1$s 立即重启 移除筛选 @@ -1213,18 +1211,7 @@ 你从信息流和搜索中静音的仓库。 已隐藏的仓库 在信息流和搜索中跳过已查看的仓库。 - 隐藏我已查看过的仓库 - 帮助我们了解哪些功能被使用。 - 应用版本。 - 功能使用计数。 - 不收集标识符。 - 不收集仓库名。 - 不收集令牌。 - 操作系统和平台。 - 共享匿名使用数据 - 我们收集的内容 - 使用数据 - 没有设置匹配 '%1$s'。 + 隐藏我已查看过的仓库 不收集标识符。 操作系统和平台。 没有设置匹配 '%1$s'。 搜索设置 添加 Forgejo 或 Gitea 主机 已添加的主机 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 18b72aead..77634414a 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1089,9 +1089,7 @@ Some changes need a restart to apply. Affected: %1$s language - theme - usage data - Restart now + theme Restart now Later Install method is Android-only Silent installers and installer attribution are Android features. Desktop installs go through the OS package manager. @@ -1276,17 +1274,7 @@ GitHub mirror Browsing history - Usage data - Share anonymous usage data - Help us understand which features get used. - What we collect - App version. - OS and platform. - Feature usage counts. - No repo names. - No tokens. - No identifiers. - Detect repo links in clipboard + Usage data Detect repo links in clipboard When you copy a github.com or codeberg.org link, we'll prompt to open it. Hide repos I've already viewed Skip seen repos in feeds and search. diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index 9f51b7294..7ca6f890a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -232,8 +232,6 @@ sealed interface TweaksAction { data class OnScopeUseMainToggled(val scope: ProxyScope, val useMain: Boolean) : TweaksAction - data class OnTelemetryToggled(val enabled: Boolean) : TweaksAction - data object OnTelemetryExpandToggle : TweaksAction data object OnClearSeenHistoryRequest : TweaksAction data object OnClearSeenHistoryDismiss : TweaksAction data object OnClearSeenHistoryConfirm : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index cf28f7c27..4b426d5ec 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -69,9 +69,7 @@ data class TweaksState( val masterProxyForm: ProxyScopeFormState = ProxyScopeFormState(), val useMasterByScope: Map = ProxyScope.entries.associateWith { false }, - val telemetryEnabled: Boolean = false, val isClearSeenHistoryDialogVisible: Boolean = false, - val telemetryExpanded: Boolean = false, ) { val restartBannerVisible: Boolean diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 11d9bbd18..924d95c7d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -109,7 +109,6 @@ class TweaksViewModel( observeNeedsRestartReasons() observeMasterProxyConfig() observeUseMasterFlags() - observeTelemetryEnabled() hasLoadedInitialData = true } @@ -1331,21 +1330,6 @@ class TweaksViewModel( } } - is TweaksAction.OnTelemetryToggled -> { - viewModelScope.launch { - runCatching { tweaksRepository.setTelemetryEnabled(action.enabled) } - runCatching { - tweaksRepository.addRestartReason( - zed.rainxch.core.domain.model.RestartReason.TELEMETRY_TOGGLE, - ) - } - } - } - - TweaksAction.OnTelemetryExpandToggle -> { - _state.update { it.copy(telemetryExpanded = !it.telemetryExpanded) } - } - TweaksAction.OnClearSeenHistoryRequest -> { _state.update { it.copy(isClearSeenHistoryDialogVisible = true) } } @@ -1481,14 +1465,6 @@ class TweaksViewModel( } } - private fun observeTelemetryEnabled() { - viewModelScope.launch { - tweaksRepository.getTelemetryEnabled().collect { enabled -> - _state.update { it.copy(telemetryEnabled = enabled) } - } - } - } - private fun mutateMasterForm(block: (ProxyScopeFormState) -> ProxyScopeFormState) { _state.update { state -> val updated = block(state.masterProxyForm).copy(isDraftDirty = true) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt index a25fd0732..6ec620ae5 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/RestartBanner.kt @@ -26,7 +26,6 @@ import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.restart_banner_body import zed.rainxch.githubstore.core.presentation.res.restart_banner_later import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_language -import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_telemetry import zed.rainxch.githubstore.core.presentation.res.restart_banner_reason_theme import zed.rainxch.githubstore.core.presentation.res.restart_banner_reasons_prefix import zed.rainxch.githubstore.core.presentation.res.restart_banner_restart_now @@ -45,7 +44,6 @@ fun RestartBanner( when (reason) { RestartReason.LANGUAGE -> Res.string.restart_banner_reason_language RestartReason.THEME_MIGRATION -> Res.string.restart_banner_reason_theme - RestartReason.TELEMETRY_TOGGLE -> Res.string.restart_banner_reason_telemetry }, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt index ff5466ec0..d8102e3ec 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/privacy/TweaksPrivacyRoot.kt @@ -55,16 +55,6 @@ import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hide_seen_bo import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hide_seen_title import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hidden_repos_body import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_hidden_repos_title -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_body -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_app_version -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_feature_usage -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_identifiers -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_repo_names -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_no_tokens -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_collect_os -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_title -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_telemetry_what_we_collect -import zed.rainxch.githubstore.core.presentation.res.tweaks_privacy_usage_data_section import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold @@ -87,15 +77,6 @@ fun TweaksPrivacyRoot( onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, showRestartBanner = state.restartBannerVisible, ) { - item(key = "telemetry_card") { - TelemetryCard( - enabled = state.telemetryEnabled, - expanded = state.telemetryExpanded, - onToggled = { viewModel.onAction(TweaksAction.OnTelemetryToggled(it)) }, - onExpandToggle = { viewModel.onAction(TweaksAction.OnTelemetryExpandToggle) }, - ) - Spacer(Modifier.height(12.dp)) - } item(key = "clipboard_card") { ToggleCard( @@ -162,110 +143,6 @@ fun TweaksPrivacyRoot( } } -@Composable -private fun TelemetryCard( - enabled: Boolean, - expanded: Boolean, - onToggled: (Boolean) -> Unit, - onExpandToggle: () -> Unit, -) { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = Radii.row, - color = MaterialTheme.colorScheme.surfaceContainerLow, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(Res.string.tweaks_privacy_usage_data_section), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.height(12.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(Radii.chip) - .clickable { onToggled(!enabled) } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(Res.string.tweaks_privacy_telemetry_title), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(Res.string.tweaks_privacy_telemetry_body), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = enabled, - onCheckedChange = null, - ) - } - - Spacer(Modifier.height(8.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(Radii.chip) - .clickable(onClick = onExpandToggle) - .padding(vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - text = stringResource(Res.string.tweaks_privacy_telemetry_what_we_collect), - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.primary, - ) - Icon( - imageVector = Icons.Default.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .size(20.dp) - .rotate(if (expanded) 180f else 0f), - ) - } - - AnimatedVisibility(visible = expanded) { - Column( - modifier = Modifier.padding(top = 4.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - listOf( - stringResource(Res.string.tweaks_privacy_telemetry_collect_app_version), - stringResource(Res.string.tweaks_privacy_telemetry_collect_os), - stringResource(Res.string.tweaks_privacy_telemetry_collect_feature_usage), - stringResource(Res.string.tweaks_privacy_telemetry_collect_no_repo_names), - stringResource(Res.string.tweaks_privacy_telemetry_collect_no_tokens), - stringResource(Res.string.tweaks_privacy_telemetry_collect_no_identifiers), - ).forEach { line -> - Text( - text = "• $line", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - } - } -} - @Composable private fun ToggleCard( title: String, From c19a3abc7adde394e09d4d873b2b387710cdf1a2 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:33:00 +0500 Subject: [PATCH 146/172] fix(tweaks): tighten hosttokens spacing --- .../presentation/hosttokens/HostTokensRoot.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt index 7cae4be97..83c0ed08a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hosttokens/HostTokensRoot.kt @@ -264,9 +264,9 @@ private fun OAuthCoexistenceNote() { Row( modifier = Modifier .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(8.dp), + .padding(horizontal = 14.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Icon( Icons.Default.Info, @@ -295,23 +295,28 @@ private fun EmptyStatePicker( verticalArrangement = Arrangement.spacedBy(8.dp), ) { item { - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(8.dp)) Text( text = stringResource(Res.string.host_tokens_empty_title), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + ), ) + Spacer(Modifier.height(4.dp)) Text( text = stringResource(Res.string.host_tokens_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(16.dp)) Text( text = stringResource(Res.string.host_tokens_picker_title), - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + ), color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(4.dp)) } items(presetForges, key = { it.tokenHost }) { kind -> PresetForgeCard( @@ -368,7 +373,7 @@ private fun PresetForgeCard( } Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), ) { GhsButton( onClick = onOpenTokenCreationPage, @@ -484,13 +489,14 @@ private fun TokenRow( .fillMaxWidth() .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Icon( Icons.Default.VpnKey, contentDescription = null, tint = MaterialTheme.colorScheme.primary, ) - Spacer(Modifier.width(12.dp)) + Spacer(Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = forge?.displayName ?: token.host, From 1e99f33b81abe208116cb4eea39ef13966854a39 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:35:03 +0500 Subject: [PATCH 147/172] i18n(auth): extract more-options labels --- .../src/commonMain/composeResources/values/strings.xml | 2 ++ .../zed/rainxch/auth/presentation/AuthenticationRoot.kt | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 77634414a..d1ba65dbe 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1070,6 +1070,8 @@ Account Mode Display + More sign-in options + Hide options Appearance Language Connection diff --git a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt index d73b71b68..6fd4d83e4 100644 --- a/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt +++ b/feature/auth/presentation/src/commonMain/kotlin/zed/rainxch/auth/presentation/AuthenticationRoot.kt @@ -104,6 +104,8 @@ import zed.rainxch.githubstore.core.presentation.res.auth_check_status import zed.rainxch.githubstore.core.presentation.res.auth_error_with_message import zed.rainxch.githubstore.core.presentation.res.auth_polling_status import zed.rainxch.githubstore.core.presentation.res.auth_rate_limited +import zed.rainxch.githubstore.core.presentation.res.auth_hide_signin_options +import zed.rainxch.githubstore.core.presentation.res.auth_more_signin_options import zed.rainxch.githubstore.core.presentation.res.continue_as_guest import zed.rainxch.githubstore.core.presentation.res.copy_code import zed.rainxch.githubstore.core.presentation.res.enter_code_on_github @@ -291,7 +293,11 @@ private fun StateLoggedOut(onAction: (AuthenticationAction) -> Unit) { GhsButton( onClick = { showMoreOptions = !showMoreOptions }, - label = if (showMoreOptions) "Hide options" else "More sign-in options", + label = if (showMoreOptions) { + stringResource(Res.string.auth_hide_signin_options) + } else { + stringResource(Res.string.auth_more_signin_options) + }, variant = GhsButtonVariant.Text, size = GhsButtonSize.Sm, trailingIcon = Icons.Default.ExpandMore, From a9531254611c7d802d11872518d5b27ba3aef574 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 16:35:04 +0500 Subject: [PATCH 148/172] i18n(auth): translate more-options labels to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 2 ++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../src/commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 2 ++ .../src/commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 2 ++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 2 ++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ 12 files changed, 24 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 059c6ad27..7c339fdad 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1307,4 +1307,6 @@ الوضع العرض ديناميكي + خيارات تسجيل دخول إضافية + إخفاء الخيارات diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 8a6a18ddc..e3809d2b3 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1269,4 +1269,6 @@ মোড ডিসপ্লে ডাইনামিক + আরও সাইন-ইন বিকল্প + বিকল্প লুকান diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 0e69e9f44..4acc2c235 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1248,4 +1248,6 @@ Modo Pantalla Dinámico + Más opciones de inicio de sesión + Ocultar opciones diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 7c3cc8f28..411940c22 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1248,4 +1248,6 @@ Mode Affichage Dynamique + Plus d'options de connexion + Masquer les options diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 7ca264690..fae38f935 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1284,4 +1284,6 @@ मोड डिस्प्ले डायनामिक + अधिक साइन-इन विकल्प + विकल्प छिपाएँ diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 4da9b28ea..dc8c3c4b4 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1282,4 +1282,6 @@ Modalità Visualizzazione Dinamico + Altre opzioni di accesso + Nascondi opzioni diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index fd1063060..4133f9e58 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1238,4 +1238,6 @@ モード 表示 ダイナミック + 他のサインイン方法 + オプションを隠す diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 8dc792daa..f0e776d54 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1268,4 +1268,6 @@ 모드 디스플레이 다이내믹 + 추가 로그인 옵션 + 옵션 숨기기 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 945e013fc..19f6af068 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1277,4 +1277,6 @@ Tryb Wyświetlanie Dynamiczny + Więcej opcji logowania + Ukryj opcje diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 01eb54f1c..be183b59f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1277,4 +1277,6 @@ Режим Отображение Динамичная + Другие способы входа + Скрыть параметры diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 970e7f6e6..90fa332fd 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1279,4 +1279,6 @@ Mod Görüntü Dinamik + Diğer giriş seçenekleri + Seçenekleri gizle diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 8806a574f..290bdac3a 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1241,4 +1241,6 @@ 模式 显示 动态 + 更多登录方式 + 隐藏选项 From 9a898bd6bbd250c497d87234d05b35b49c2fd2e8 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:09:03 +0500 Subject: [PATCH 149/172] feat(tweaks): discovery platforms picker in Sources --- .../composeResources/values/strings.xml | 2 + .../tweaks/presentation/TweaksAction.kt | 4 + .../tweaks/presentation/TweaksState.kt | 2 + .../tweaks/presentation/TweaksViewModel.kt | 21 ++++ .../presentation/sources/TweaksSourcesRoot.kt | 103 ++++++++++++++++++ 5 files changed, 132 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index d1ba65dbe..ad11848d1 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1072,6 +1072,8 @@ Display More sign-in options Hide options + Discovery platforms + Filter Home feed to specific platforms. Leave all off to see everything. Appearance Language Connection diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index 7ca6f890a..839a68acc 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -210,6 +210,10 @@ sealed interface TweaksAction { data object OnAddCustomForge : TweaksAction data class OnRemoveCustomForge(val host: String) : TweaksAction + data class OnDiscoveryPlatformToggled( + val platform: zed.rainxch.core.domain.model.DiscoveryPlatform, + ) : TweaksAction + data object OnRestartNowClick : TweaksAction data object OnRestartLaterClick : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt index 4b426d5ec..5f70e1b2b 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt @@ -3,6 +3,7 @@ package zed.rainxch.tweaks.presentation import zed.rainxch.core.domain.model.AppTheme import zed.rainxch.core.domain.model.ContentWidth import zed.rainxch.core.domain.model.DhizukuAvailability +import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.FontTheme import zed.rainxch.core.domain.model.InstallerAttribution import zed.rainxch.core.domain.model.InstallerType @@ -70,6 +71,7 @@ data class TweaksState( val useMasterByScope: Map = ProxyScope.entries.associateWith { false }, val isClearSeenHistoryDialogVisible: Boolean = false, + val selectedDiscoveryPlatforms: Set = emptySet(), ) { val restartBannerVisible: Boolean diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 924d95c7d..1b609a0af 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -109,6 +109,7 @@ class TweaksViewModel( observeNeedsRestartReasons() observeMasterProxyConfig() observeUseMasterFlags() + observeDiscoveryPlatforms() hasLoadedInitialData = true } @@ -1166,6 +1167,18 @@ class TweaksViewModel( } } + is TweaksAction.OnDiscoveryPlatformToggled -> { + viewModelScope.launch { + val current = _state.value.selectedDiscoveryPlatforms + val next = if (action.platform in current) { + current - action.platform + } else { + current + action.platform + } + runCatching { tweaksRepository.setDiscoveryPlatforms(next) } + } + } + is TweaksAction.OnRemoveCustomForge -> { viewModelScope.launch { val result = runCatching { tweaksRepository.removeCustomForgeHost(action.host) } @@ -1465,6 +1478,14 @@ class TweaksViewModel( } } + private fun observeDiscoveryPlatforms() { + viewModelScope.launch { + tweaksRepository.getDiscoveryPlatforms().collect { platforms -> + _state.update { it.copy(selectedDiscoveryPlatforms = platforms) } + } + } + } + private fun mutateMasterForm(block: (ProxyScopeFormState) -> ProxyScopeFormState) { _state.update { state -> val updated = block(state.masterProxyForm).copy(isDraftDirty = true) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt index dfe0e8550..ff8f0095c 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/sources/TweaksSourcesRoot.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -39,9 +41,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.core.presentation.theme.tokens.GhsAccents import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.platform_section_android +import zed.rainxch.githubstore.core.presentation.res.platform_section_linux +import zed.rainxch.githubstore.core.presentation.res.platform_section_macos +import zed.rainxch.githubstore.core.presentation.res.platform_section_windows +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_platforms_body +import zed.rainxch.githubstore.core.presentation.res.tweaks_sources_platforms_title import zed.rainxch.githubstore.core.presentation.res.custom_forges_count import zed.rainxch.githubstore.core.presentation.res.custom_forges_entry_label import zed.rainxch.githubstore.core.presentation.res.remove_search_history_item @@ -125,6 +134,14 @@ fun TweaksSourcesRoot( accent = GhsAccents.Mint, onClick = { viewModel.onAction(TweaksAction.OnOpenCustomForgesDialog) }, ) + Spacer(Modifier.height(16.dp)) + } + + item(key = "platforms_card") { + DiscoveryPlatformsCard( + selected = state.selectedDiscoveryPlatforms, + onToggle = { viewModel.onAction(TweaksAction.OnDiscoveryPlatformToggled(it)) }, + ) } if (state.customForgeHosts.isNotEmpty()) { @@ -160,6 +177,92 @@ fun TweaksSourcesRoot( } } +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DiscoveryPlatformsCard( + selected: Set, + onToggle: (DiscoveryPlatform) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(Res.string.tweaks_sources_platforms_title), + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(Res.string.tweaks_sources_platforms_body), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(12.dp)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + DiscoveryPlatform.selectablePlatforms.forEach { platform -> + PlatformChip( + label = stringResource(platform.labelRes()), + isSelected = platform in selected, + onClick = { onToggle(platform) }, + ) + } + } + } + } +} + +@Composable +private fun PlatformChip( + label: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + val container = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + val content = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + } + Box( + modifier = Modifier + .clip(Radii.chip) + .background(container) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + ) + } +} + +private fun DiscoveryPlatform.labelRes() = when (this) { + DiscoveryPlatform.Android -> Res.string.platform_section_android + DiscoveryPlatform.Macos -> Res.string.platform_section_macos + DiscoveryPlatform.Windows -> Res.string.platform_section_windows + DiscoveryPlatform.Linux -> Res.string.platform_section_linux + DiscoveryPlatform.All -> Res.string.platform_section_android +} + @Composable private fun DrillRow( icon: ImageVector, From ac99a55c3c4d76b5fc0398268d5be6610531d825 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:09:04 +0500 Subject: [PATCH 150/172] i18n(tweaks): translate discovery platforms strings to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 2 ++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../src/commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 2 ++ .../src/commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 2 ++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 2 ++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ 12 files changed, 24 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 7c339fdad..614068ff3 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1309,4 +1309,6 @@ ديناميكي خيارات تسجيل دخول إضافية إخفاء الخيارات + منصات الاكتشاف + تصفية الصفحة الرئيسية حسب المنصة. اترك الكل بدون تحديد لعرض كل شيء. diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index e3809d2b3..91ac27c11 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1271,4 +1271,6 @@ ডাইনামিক আরও সাইন-ইন বিকল্প বিকল্প লুকান + ডিসকভারি প্ল্যাটফর্ম + হোম ফিডকে নির্দিষ্ট প্ল্যাটফর্মে ফিল্টার করুন। সব বন্ধ রাখলে সব দেখাবে। diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 4acc2c235..4fa922188 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1250,4 +1250,6 @@ Dinámico Más opciones de inicio de sesión Ocultar opciones + Plataformas de descubrimiento + Filtra el feed principal por plataforma. Déjalas todas desactivadas para verlo todo. diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 411940c22..0cb3571dc 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1250,4 +1250,6 @@ Dynamique Plus d'options de connexion Masquer les options + Plateformes de découverte + Filtre le fil d'accueil par plateforme. Laisse tout désactivé pour tout voir. diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index fae38f935..c21377b55 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1286,4 +1286,6 @@ डायनामिक अधिक साइन-इन विकल्प विकल्प छिपाएँ + डिस्कवरी प्लेटफ़ॉर्म + होम फ़ीड को विशिष्ट प्लेटफ़ॉर्म पर फ़िल्टर करें। सब बंद रहने पर सब कुछ दिखेगा। diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index dc8c3c4b4..54e18dc36 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1284,4 +1284,6 @@ Dinamico Altre opzioni di accesso Nascondi opzioni + Piattaforme di scoperta + Filtra il feed Home per piattaforma. Lasciale tutte disattivate per vedere tutto. diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 4133f9e58..1cfd98c8f 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1240,4 +1240,6 @@ ダイナミック 他のサインイン方法 オプションを隠す + ディスカバリー対象プラットフォーム + ホームフィードを特定のプラットフォームで絞り込みます。すべてオフで全表示。 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index f0e776d54..48c13e57c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1270,4 +1270,6 @@ 다이내믹 추가 로그인 옵션 옵션 숨기기 + 디스커버리 플랫폼 + 홈 피드를 특정 플랫폼으로 필터링합니다. 모두 꺼두면 전체 표시. diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 19f6af068..fb25a0e7d 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1279,4 +1279,6 @@ Dynamiczny Więcej opcji logowania Ukryj opcje + Platformy odkrywania + Filtruj kanał Home według platformy. Pozostaw wszystkie wyłączone, aby zobaczyć wszystko. diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index be183b59f..d17500740 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1279,4 +1279,6 @@ Динамичная Другие способы входа Скрыть параметры + Платформы для подборки + Фильтрация главной ленты по платформам. Если все выключены, показываются все. diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 90fa332fd..79c73baec 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1281,4 +1281,6 @@ Dinamik Diğer giriş seçenekleri Seçenekleri gizle + Keşif platformları + Ana akışı platforma göre filtrele. Hepsini kapalı bırakırsan her şey gösterilir. diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 290bdac3a..e1a6e4f3a 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1243,4 +1243,6 @@ 动态 更多登录方式 隐藏选项 + 发现平台 + 按平台筛选首页内容。全部关闭则显示全部。 From bca629419bfe0336ad904d1d03b8e591d822bbbf Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:14:40 +0500 Subject: [PATCH 151/172] feat(feedback): platform-aware sheet, dialog on desktop --- .../components/FeedbackBottomSheet.kt | 216 ++++++++++++------ 1 file changed, 140 insertions(+), 76 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt index ff160f1ad..89f0cffe4 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/FeedbackBottomSheet.kt @@ -1,14 +1,17 @@ package zed.rainxch.tweaks.presentation.feedback.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -24,12 +27,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.domain.getPlatform +import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.theme.shapes.WonkySquircleShape import zed.rainxch.core.presentation.utils.ObserveAsEvents import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.feedback_close @@ -38,6 +47,7 @@ import zed.rainxch.githubstore.core.presentation.res.feedback_field_title import zed.rainxch.githubstore.core.presentation.res.feedback_title import zed.rainxch.tweaks.presentation.feedback.FeedbackAction import zed.rainxch.tweaks.presentation.feedback.FeedbackEvent +import zed.rainxch.tweaks.presentation.feedback.FeedbackState import zed.rainxch.tweaks.presentation.feedback.FeedbackViewModel import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel @@ -50,7 +60,6 @@ fun FeedbackBottomSheet( viewModel: FeedbackViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ObserveAsEvents(viewModel.events) { event -> when (event) { @@ -59,92 +68,147 @@ fun FeedbackBottomSheet( } } - ModalBottomSheet( - onDismissRequest = { - viewModel.onAction(FeedbackAction.OnDismiss) - onDismiss() - }, - sheetState = sheetState, - modifier = Modifier.fillMaxSize(), - ) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + val dismiss = { + viewModel.onAction(FeedbackAction.OnDismiss) + onDismiss() + } + + if (getPlatform() == Platform.ANDROID) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = dismiss, + sheetState = sheetState, ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + FeedbackContent( + state = state, + onAction = viewModel::onAction, + onDismiss = dismiss, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) + } + } else { + Dialog( + onDismissRequest = dismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Box( + modifier = Modifier + .widthIn(max = 560.dp) + .heightIn(max = 720.dp) + .clip(WonkySquircleShape.Dialog) + .background(MaterialTheme.colorScheme.surface), ) { - Text( - text = stringResource(Res.string.feedback_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + FeedbackContent( + state = state, + onAction = viewModel::onAction, + onDismiss = dismiss, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 20.dp), ) - IconButton(onClick = { - viewModel.onAction(FeedbackAction.OnDismiss) - onDismiss() - }) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(Res.string.feedback_close), - modifier = Modifier.size(24.dp), - ) - } } + } + } +} - CategorySelector( - selected = state.category, - onSelected = { viewModel.onAction(FeedbackAction.OnCategoryChange(it)) }, - ) +@Composable +private fun FeedbackContent( + state: FeedbackState, + onAction: (FeedbackAction) -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + Column( + modifier = modifier.verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + FeedbackHeader(onDismiss = onDismiss) - TopicSelector( - selected = state.topic, - onSelected = { viewModel.onAction(FeedbackAction.OnTopicChange(it)) }, - ) + SectionLabel(text = stringResource(Res.string.feedback_field_title) + " *", topGap = 0.dp) + GhsTextField( + value = state.title, + onValueChange = { onAction(FeedbackAction.OnTitleChange(it)) }, + label = stringResource(Res.string.feedback_field_title), + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) - GhsTextField( - value = state.title, - onValueChange = { viewModel.onAction(FeedbackAction.OnTitleChange(it)) }, - label = stringResource(Res.string.feedback_field_title) + " *", - singleLine = true, - modifier = Modifier.fillMaxWidth(), - ) + SectionLabel(text = stringResource(Res.string.feedback_field_description) + " *") + GhsTextField( + value = state.description, + onValueChange = { onAction(FeedbackAction.OnDescriptionChange(it)) }, + label = stringResource(Res.string.feedback_field_description), + singleLine = false, + minLines = 4, + modifier = Modifier.fillMaxWidth(), + ) - GhsTextField( - value = state.description, - onValueChange = { viewModel.onAction(FeedbackAction.OnDescriptionChange(it)) }, - label = stringResource(Res.string.feedback_field_description) + " *", - singleLine = false, - minLines = 4, - modifier = Modifier.fillMaxWidth(), - ) + CategorySelector( + selected = state.category, + onSelected = { onAction(FeedbackAction.OnCategoryChange(it)) }, + ) - ConditionalFields( - state = state, - onAction = viewModel::onAction, - ) + TopicSelector( + selected = state.topic, + onSelected = { onAction(FeedbackAction.OnTopicChange(it)) }, + ) - DiagnosticsPreview( - diagnostics = state.diagnostics, - channel = FeedbackChannel.GITHUB, - enabled = state.attachDiagnostics, - onToggle = { viewModel.onAction(FeedbackAction.OnAttachDiagnosticsToggle) }, - ) + ConditionalFields(state = state, onAction = onAction) - SendActions( - canSend = state.canSend, - isSending = state.isSending, - onSendEmail = { viewModel.onAction(FeedbackAction.OnSendViaEmail) }, - onSendGithub = { viewModel.onAction(FeedbackAction.OnSendViaGithub) }, - ) + DiagnosticsPreview( + diagnostics = state.diagnostics, + channel = FeedbackChannel.GITHUB, + enabled = state.attachDiagnostics, + onToggle = { onAction(FeedbackAction.OnAttachDiagnosticsToggle) }, + ) + + SendActions( + canSend = state.canSend, + isSending = state.isSending, + onSendEmail = { onAction(FeedbackAction.OnSendViaEmail) }, + onSendGithub = { onAction(FeedbackAction.OnSendViaGithub) }, + ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(8.dp)) + } +} + +@Composable +private fun FeedbackHeader(onDismiss: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(Res.string.feedback_title), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(Res.string.feedback_close), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } + +@Composable +private fun SectionLabel(text: String, topGap: androidx.compose.ui.unit.Dp = 4.dp) { + Column { + if (topGap > 0.dp) Spacer(Modifier.height(topGap)) + Text( + text = text, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} From 804dfc8776029cd656e3bfb2573f339d6c01f356 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:23:14 +0500 Subject: [PATCH 152/172] feat(feedback): pill chips for category and topic selectors --- .../feedback/components/CategorySelector.kt | 106 ++++++++++++------ .../feedback/components/TopicSelector.kt | 25 ++--- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/CategorySelector.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/CategorySelector.kt index a08b19554..8a3c8f1f1 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/CategorySelector.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/CategorySelector.kt @@ -1,25 +1,34 @@ package zed.rainxch.tweaks.presentation.feedback.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.Role +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.feedback_category_label import zed.rainxch.tweaks.presentation.feedback.model.FeedbackCategory +@OptIn(ExperimentalLayoutApi::class) @Composable fun CategorySelector( selected: FeedbackCategory, @@ -29,41 +38,66 @@ fun CategorySelector( Column(modifier = modifier) { Text( text = stringResource(Res.string.feedback_category_label), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - - Column( - modifier = Modifier - .fillMaxWidth() - .selectableGroup() - .padding(top = 8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), + Spacer(Modifier.height(8.dp)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { FeedbackCategory.entries.forEach { category -> - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = category == selected, - onClick = { onSelected(category) }, - role = Role.RadioButton, - ) - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - RadioButton( - selected = category == selected, - onClick = null, - ) - Text( - text = stringResource(category.label), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - } + FeedbackPillChip( + label = stringResource(category.label), + isSelected = category == selected, + onClick = { onSelected(category) }, + ) } } } } + +@Composable +internal fun FeedbackPillChip( + label: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + val container by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + animationSpec = tween(durationMillis = 180), + label = "chip_container", + ) + val content by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + animationSpec = tween(durationMillis = 180), + label = "chip_content", + ) + Box( + modifier = Modifier + .clip(Radii.chip) + .background(container) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + ) + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/TopicSelector.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/TopicSelector.kt index fcc9f9ff4..ce3d84656 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/TopicSelector.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/TopicSelector.kt @@ -4,14 +4,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.FilterChip +import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.githubstore.core.presentation.res.Res @@ -28,23 +28,22 @@ fun TopicSelector( Column(modifier = modifier) { Text( text = stringResource(Res.string.feedback_topic_label), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - + Spacer(Modifier.height(8.dp)) FlowRow( - modifier = Modifier - .fillMaxWidth() - .selectableGroup() - .padding(top = 8.dp), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { FeedbackTopic.entries.forEach { topic -> - FilterChip( - selected = topic == selected, + FeedbackPillChip( + label = stringResource(topic.label), + isSelected = topic == selected, onClick = { onSelected(topic) }, - label = { Text(stringResource(topic.label)) }, ) } } From b7a0701c319c8780020f6bd8c780646ec2a514cd Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:27:01 +0500 Subject: [PATCH 153/172] feat(feedback): include theme palette and mode in diagnostics --- .../tweaks/presentation/feedback/FeedbackViewModel.kt | 11 +++++++++++ .../feedback/components/DiagnosticsPreview.kt | 3 ++- .../presentation/feedback/model/DiagnosticsInfo.kt | 2 ++ .../presentation/feedback/util/FeedbackComposer.kt | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt index a8cc921d5..9ee38650e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/FeedbackViewModel.kt @@ -118,6 +118,15 @@ class FeedbackViewModel( } val user = userSessionRepository.getUser().firstOrNull() val appLanguage = tweaksRepository.getAppLanguage().firstOrNull() + val palette = tweaksRepository.getThemeColor().firstOrNull() + val isDark = tweaksRepository.getIsDarkTheme().firstOrNull() + val amoled = tweaksRepository.getAmoledTheme().firstOrNull() ?: false + val themeMode = when { + isDark == null -> "System" + isDark && amoled -> "AMOLED" + isDark -> "Dark" + else -> "Light" + } return DiagnosticsInfo( appVersion = appVersionInfo.versionName, platform = platform.displayName(), @@ -125,6 +134,8 @@ class FeedbackViewModel( locale = appLanguage ?: getSystemLocaleTag(), installerType = installerString, githubUsername = user?.username, + themePalette = palette?.name ?: "Nord", + themeMode = themeMode, ) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/DiagnosticsPreview.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/DiagnosticsPreview.kt index 7240f7456..77450248e 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/DiagnosticsPreview.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/components/DiagnosticsPreview.kt @@ -79,7 +79,8 @@ private fun formatDiagnostics(d: DiagnosticsInfo, channel: FeedbackChannel): Str val sb = StringBuilder() sb.append("- App: GitHub Store v").append(d.appVersion).append('\n') sb.append("- Platform: ").append(d.platform).append(' ').append(d.osVersion).append('\n') - sb.append("- Locale: ").append(d.locale) + sb.append("- Locale: ").append(d.locale).append('\n') + sb.append("- Theme: ").append(d.themePalette).append(" / ").append(d.themeMode) d.installerType?.let { sb.append('\n').append("- Installer: ").append(it) } if (channel == FeedbackChannel.GITHUB) { d.githubUsername?.let { sb.append('\n').append("- GitHub user: @").append(it) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/model/DiagnosticsInfo.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/model/DiagnosticsInfo.kt index 2cee66709..f5aef8bc4 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/model/DiagnosticsInfo.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/model/DiagnosticsInfo.kt @@ -7,4 +7,6 @@ data class DiagnosticsInfo( val locale: String, val installerType: String?, val githubUsername: String?, + val themePalette: String, + val themeMode: String, ) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt index b7b0c0144..02eb18b83 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt @@ -47,6 +47,7 @@ object FeedbackComposer { builder.append("- App: GitHub Store v").append(d.appVersion).append('\n') builder.append("- Platform: ").append(d.platform).append(' ').append(d.osVersion).append('\n') builder.append("- Locale: ").append(d.locale).append('\n') + builder.append("- Theme: ").append(d.themePalette).append(" / ").append(d.themeMode).append('\n') d.installerType?.let { builder.append("- Installer: ").append(it).append('\n') } if (channel == FeedbackChannel.GITHUB) { d.githubUsername?.let { builder.append("- GitHub user: @").append(it).append('\n') } From 91f611c0a373f95e8c1f79fe8139c3bebf966f20 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:29:21 +0500 Subject: [PATCH 154/172] feat(feedback): prepend category and topic to issue body --- .../feedback/util/FeedbackComposer.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt index 02eb18b83..252c0b5f8 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/feedback/util/FeedbackComposer.kt @@ -5,6 +5,7 @@ import io.ktor.http.encodeURLParameter import zed.rainxch.tweaks.presentation.feedback.FeedbackState import zed.rainxch.tweaks.presentation.feedback.model.FeedbackCategory import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel +import zed.rainxch.tweaks.presentation.feedback.model.FeedbackTopic object FeedbackComposer { const val FEEDBACK_EMAIL = "hello@github-store.org" @@ -23,6 +24,9 @@ object FeedbackComposer { fun composeBody(state: FeedbackState, channel: FeedbackChannel): String { val builder = StringBuilder() + builder.append("**Type:** ").append(state.category.displayLabel()) + .append(" · **Area:** ").append(state.topic.displayLabel()) + builder.appendSection("Description", state.description) when (state.category) { @@ -65,6 +69,24 @@ object FeedbackComposer { append("## ").append(title).append('\n').append(trimmed) } + private fun FeedbackCategory.displayLabel(): String = when (this) { + FeedbackCategory.BUG -> "Bug" + FeedbackCategory.FEATURE_REQUEST -> "Feature request" + FeedbackCategory.CHANGE_REQUEST -> "Change request" + FeedbackCategory.OTHER -> "Other" + } + + private fun FeedbackTopic.displayLabel(): String = when (this) { + FeedbackTopic.INSTALL_UPDATE -> "Install & updates" + FeedbackTopic.SEARCH_DISCOVERY -> "Search & discovery" + FeedbackTopic.REPO_DETAILS -> "Repo details" + FeedbackTopic.AUTH_ACCOUNT -> "Auth & account" + FeedbackTopic.UI_UX -> "UI / UX" + FeedbackTopic.TRANSLATION -> "Translation" + FeedbackTopic.PERFORMANCE -> "Performance" + FeedbackTopic.OTHER -> "Other" + } + private fun String.truncateToCap(): String { if (length <= BODY_MAX_CHARS) return this val suffix = "\n\n…[truncated]" From f90e47bb2f37ac8d8d8663c4d3b6356c79332429 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:35:25 +0500 Subject: [PATCH 155/172] feat(appinfo): brand icons for community tiles via simpleicons --- feature/tweaks/presentation/build.gradle.kts | 3 ++ .../presentation/appinfo/TweaksAppInfoRoot.kt | 36 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/feature/tweaks/presentation/build.gradle.kts b/feature/tweaks/presentation/build.gradle.kts index 5990ccfc8..2a2d4c242 100644 --- a/feature/tweaks/presentation/build.gradle.kts +++ b/feature/tweaks/presentation/build.gradle.kts @@ -18,6 +18,9 @@ kotlin { implementation(libs.touchlab.kermit) implementation(libs.kotlinx.serialization.json) + implementation(libs.coil3.compose) + implementation(libs.coil3.svg) + implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index 5cb5a1ee5..2054bf73a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -280,21 +280,21 @@ private fun CommunityCard( ) { SocialTile( label = "Telegram", - icon = Icons.AutoMirrored.Filled.Send, + iconUrl = "https://cdn.simpleicons.org/telegram/000000", accent = GhsAccents.Sky, onClick = onTelegram, modifier = Modifier.weight(1f), ) SocialTile( label = "Discord", - icon = Icons.Outlined.Forum, + iconUrl = "https://cdn.simpleicons.org/discord/000000", accent = GhsAccents.Periwinkle, onClick = onDiscord, modifier = Modifier.weight(1f), ) SocialTile( label = "Mastodon", - icon = Icons.Outlined.AlternateEmail, + iconUrl = "https://cdn.simpleicons.org/mastodon/000000", accent = GhsAccents.Lavender, onClick = onMastodon, modifier = Modifier.weight(1f), @@ -307,21 +307,21 @@ private fun CommunityCard( ) { SocialTile( label = "Reddit", - icon = Icons.Outlined.Public, + iconUrl = "https://cdn.simpleicons.org/reddit/000000", accent = GhsAccents.Peach, onClick = onReddit, modifier = Modifier.weight(1f), ) SocialTile( label = "GitHub", - icon = Icons.Outlined.Code, + iconUrl = "https://cdn.simpleicons.org/github/000000", accent = GhsAccents.Tan, onClick = onGithub, modifier = Modifier.weight(1f), ) SocialTile( label = "Website", - icon = Icons.Outlined.Language, + iconFallback = Icons.Outlined.Language, accent = GhsAccents.Sage, onClick = onWebsite, modifier = Modifier.weight(1f), @@ -366,10 +366,11 @@ private fun CommunityCard( @Composable private fun SocialTile( label: String, - icon: ImageVector, accent: Color, onClick: () -> Unit, modifier: Modifier = Modifier, + iconUrl: String? = null, + iconFallback: ImageVector? = null, ) { Surface( modifier = modifier @@ -393,12 +394,21 @@ private fun SocialTile( .background(accent.copy(alpha = 0.22f)), contentAlignment = Alignment.Center, ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = accent, - modifier = Modifier.size(20.dp), - ) + if (iconUrl != null) { + coil3.compose.AsyncImage( + model = iconUrl, + contentDescription = null, + modifier = Modifier.size(20.dp), + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(accent), + ) + } else if (iconFallback != null) { + Icon( + imageVector = iconFallback, + contentDescription = null, + tint = accent, + modifier = Modifier.size(20.dp), + ) + } } Text( text = label, From 53d5e913f7500583f6ce2730931cec1f73c1b780 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:44:17 +0500 Subject: [PATCH 156/172] refactor(ia): move app info from tweaks to profile as about --- .../kotlin/zed/rainxch/githubstore/Main.kt | 4 ++-- .../githubstore/app/deeplink/DeepLinkParser.kt | 6 +++--- .../githubstore/app/navigation/AppNavigation.kt | 15 ++++----------- .../app/navigation/GithubStoreGraph.kt | 2 +- .../composeResources/values/strings.xml | 2 ++ .../rainxch/profile/presentation/ProfileAction.kt | 2 ++ .../rainxch/profile/presentation/ProfileRoot.kt | 5 +++++ .../profile/presentation/ProfileViewModel.kt | 4 ++++ .../presentation/components/ProfileSections.kt | 11 +++++++++++ .../zed/rainxch/tweaks/presentation/TweaksRoot.kt | 11 ----------- .../presentation/appinfo/TweaksAppInfoRoot.kt | 12 ------------ 11 files changed, 34 insertions(+), 40 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt index e9d289af5..0abc22c0e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.kt @@ -108,8 +108,8 @@ fun App(deepLinkUri: String? = null) { } } - DeepLinkDestination.TweaksAppInfo -> { - navController.navigate(GithubStoreGraph.TweaksAppInfoScreen) { + DeepLinkDestination.About -> { + navController.navigate(GithubStoreGraph.AboutScreen) { launchSingleTop = true } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt index 50d110313..cdb22380e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/deeplink/DeepLinkParser.kt @@ -20,7 +20,7 @@ sealed interface DeepLinkDestination { data object Tweaks : DeepLinkDestination - data object TweaksAppInfo : DeepLinkDestination + data object About : DeepLinkDestination data object TweaksLicenses : DeepLinkDestination @@ -109,8 +109,8 @@ object DeepLinkParser { DeepLinkDestination.Tweaks } - uri == "githubstore://tweaks/app-info" -> { - DeepLinkDestination.TweaksAppInfo + uri == "githubstore://about" || uri == "githubstore://tweaks/app-info" -> { + DeepLinkDestination.About } uri == "githubstore://tweaks/licenses" -> { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index f74719adb..3a1856b96 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -603,6 +603,9 @@ fun AppNavigation( onNavigateToTweaks = { navController.navigate(GithubStoreGraph.TweaksScreen) }, + onNavigateToAbout = { + navController.navigate(GithubStoreGraph.AboutScreen) + }, hasUnreadAnnouncements = announcementsUnreadCount > 0, ) } @@ -717,11 +720,6 @@ fun AppNavigation( launchSingleTop = true } }, - onNavigateToAppInfo = { - navController.navigate(GithubStoreGraph.TweaksAppInfoScreen) { - launchSingleTop = true - } - }, ) } @@ -794,14 +792,9 @@ fun AppNavigation( ) } - composable { + composable { TweaksAppInfoRoot( onNavigateBack = { navController.popBackStack() }, - onNavigateToWhatsNewHistory = { - navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) { - launchSingleTop = true - } - }, onNavigateToLicenses = { navController.navigate(GithubStoreGraph.LicensesScreen) { launchSingleTop = true diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index 6751602a8..0bbc46548 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -63,7 +63,7 @@ sealed interface GithubStoreGraph { data object TweaksPrivacyScreen : GithubStoreGraph @Serializable - data object TweaksAppInfoScreen : GithubStoreGraph + data object AboutScreen : GithubStoreGraph @Serializable data object LicensesScreen : GithubStoreGraph diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index ad11848d1..91c19c575 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -1074,6 +1074,8 @@ Hide options Discovery platforms Filter Home feed to specific platforms. Leave all off to see everything. + About + Version, community, legal Appearance Language Connection diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt index c861a774b..781e7c7ec 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt @@ -25,4 +25,6 @@ sealed interface ProfileAction { data object OnAnnouncementsLongClick : ProfileAction data object OnTweaksClick : ProfileAction + + data object OnAboutClick : ProfileAction } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 0d03f1692..2f3d0bbdd 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -57,6 +57,7 @@ fun ProfileRoot( onNavigateToAnnouncements: () -> Unit, onPreviewAnnouncements: () -> Unit, onNavigateToTweaks: () -> Unit, + onNavigateToAbout: () -> Unit, hasUnreadAnnouncements: Boolean, viewModel: ProfileViewModel = koinViewModel(), ) { @@ -157,6 +158,10 @@ fun ProfileRoot( onNavigateToTweaks() } + ProfileAction.OnAboutClick -> { + onNavigateToAbout() + } + else -> { viewModel.onAction(action) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 5f6bb4003..ca8095e6c 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -152,6 +152,10 @@ class ProfileViewModel( ProfileAction.OnTweaksClick -> { } + + ProfileAction.OnAboutClick -> { + + } } } } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt index 8e584599c..8ea914d9a 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/ProfileSections.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.outlined.Campaign import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material.icons.outlined.Star @@ -142,6 +143,16 @@ fun LazyListScope.profileSections( accentColor = GhsAccents.Sage, onClick = { onAction(ProfileAction.OnTweaksClick) }, ) + Spacer(Modifier.height(8.dp)) + } + item(key = "row_about") { + GhsEntryRow( + title = stringResource(Res.string.profile_entry_about_title), + subtitle = stringResource(Res.string.profile_entry_about_subtitle), + icon = Icons.Outlined.Info, + accentColor = GhsAccents.Aqua, + onClick = { onAction(ProfileAction.OnAboutClick) }, + ) } if (state.isUserLoggedIn) { diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 4d1b316cd..f588f3005 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -87,7 +87,6 @@ fun TweaksRoot( onNavigateToStorage: () -> Unit, onNavigateToPrivacy: () -> Unit, onNavigateToHostTokens: () -> Unit, - onNavigateToAppInfo: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -141,7 +140,6 @@ fun TweaksRoot( onNavigateToStorage = onNavigateToStorage, onNavigateToPrivacy = onNavigateToPrivacy, onNavigateToHostTokens = onNavigateToHostTokens, - onNavigateToAppInfo = onNavigateToAppInfo, onSendFeedbackClick = { feedbackSheetOpen = true }, onRestartNow = { viewModel.onAction(TweaksAction.OnRestartNowClick) }, onRestartLater = { viewModel.onAction(TweaksAction.OnRestartLaterClick) }, @@ -200,7 +198,6 @@ fun TweaksHubScreen( onNavigateToStorage: () -> Unit, onNavigateToPrivacy: () -> Unit, onNavigateToHostTokens: () -> Unit, - onNavigateToAppInfo: () -> Unit, onSendFeedbackClick: () -> Unit, onRestartNow: () -> Unit, onRestartLater: () -> Unit, @@ -310,13 +307,6 @@ fun TweaksHubScreen( TweaksHubBlock( title = stringResource(Res.string.section_app_block), entries = listOf( - TweaksHubEntry( - title = stringResource(Res.string.tweaks_entry_app_info), - subtitle = state.versionName.ifBlank { "—" }, - icon = Icons.Outlined.Info, - onClick = onNavigateToAppInfo, - accent = GhsAccents.Aqua, - ), TweaksHubEntry( title = stringResource(Res.string.tweaks_entry_feedback), subtitle = stringResource(Res.string.feedback_hub_subtitle), @@ -500,7 +490,6 @@ private fun Preview() { onNavigateToStorage = {}, onNavigateToPrivacy = {}, onNavigateToHostTokens = {}, - onNavigateToAppInfo = {}, onSendFeedbackClick = {}, onRestartNow = {}, onRestartLater = {}, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index 2054bf73a..1e4ad9c60 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -89,7 +89,6 @@ private const val BUSINESS_EMAIL = "mailto:contact@github-store.org" @Composable fun TweaksAppInfoRoot( onNavigateBack: () -> Unit, - onNavigateToWhatsNewHistory: () -> Unit, onNavigateToLicenses: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { @@ -129,17 +128,6 @@ fun TweaksAppInfoRoot( Spacer(Modifier.height(20.dp)) } - item(key = "action_whats_new") { - ActionRow( - icon = Icons.Outlined.NewReleases, - title = stringResource(Res.string.tweaks_app_info_whats_new_title), - subtitle = stringResource(Res.string.tweaks_app_info_whats_new_subtitle), - accent = GhsAccents.Peach, - onClick = onNavigateToWhatsNewHistory, - ) - Spacer(Modifier.height(8.dp)) - } - item(key = "action_licenses") { ActionRow( icon = Icons.Outlined.Code, From bf46dca186aae1158623f7c45cebe9f95f718ddd Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 17:44:18 +0500 Subject: [PATCH 157/172] i18n(profile): translate About row to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 2 ++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../src/commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 2 ++ .../src/commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 2 ++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 2 ++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ 12 files changed, 24 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 614068ff3..cd6467389 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1311,4 +1311,6 @@ إخفاء الخيارات منصات الاكتشاف تصفية الصفحة الرئيسية حسب المنصة. اترك الكل بدون تحديد لعرض كل شيء. + حول + الإصدار، المجتمع، القانوني diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 91ac27c11..ba04406fa 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1273,4 +1273,6 @@ বিকল্প লুকান ডিসকভারি প্ল্যাটফর্ম হোম ফিডকে নির্দিষ্ট প্ল্যাটফর্মে ফিল্টার করুন। সব বন্ধ রাখলে সব দেখাবে। + সম্পর্কে + সংস্করণ, কমিউনিটি, আইনি diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 4fa922188..70995cf67 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1252,4 +1252,6 @@ Ocultar opciones Plataformas de descubrimiento Filtra el feed principal por plataforma. Déjalas todas desactivadas para verlo todo. + Acerca de + Versión, comunidad, legal diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 0cb3571dc..730291a7d 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1252,4 +1252,6 @@ Masquer les options Plateformes de découverte Filtre le fil d'accueil par plateforme. Laisse tout désactivé pour tout voir. + À propos + Version, communauté, mentions légales diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index c21377b55..e6881e082 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1288,4 +1288,6 @@ विकल्प छिपाएँ डिस्कवरी प्लेटफ़ॉर्म होम फ़ीड को विशिष्ट प्लेटफ़ॉर्म पर फ़िल्टर करें। सब बंद रहने पर सब कुछ दिखेगा। + के बारे में + संस्करण, समुदाय, कानूनी diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 54e18dc36..a6b053696 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1286,4 +1286,6 @@ Nascondi opzioni Piattaforme di scoperta Filtra il feed Home per piattaforma. Lasciale tutte disattivate per vedere tutto. + Informazioni + Versione, community, legale diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 1cfd98c8f..b0dde97b6 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1242,4 +1242,6 @@ オプションを隠す ディスカバリー対象プラットフォーム ホームフィードを特定のプラットフォームで絞り込みます。すべてオフで全表示。 + アプリ情報 + バージョン、コミュニティ、法的情報 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 48c13e57c..8dc399c59 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1272,4 +1272,6 @@ 옵션 숨기기 디스커버리 플랫폼 홈 피드를 특정 플랫폼으로 필터링합니다. 모두 꺼두면 전체 표시. + 정보 + 버전, 커뮤니티, 법적 고지 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index fb25a0e7d..c15dfa0cf 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1281,4 +1281,6 @@ Ukryj opcje Platformy odkrywania Filtruj kanał Home według platformy. Pozostaw wszystkie wyłączone, aby zobaczyć wszystko. + Informacje + Wersja, społeczność, informacje prawne diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index d17500740..10920f5c6 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1281,4 +1281,6 @@ Скрыть параметры Платформы для подборки Фильтрация главной ленты по платформам. Если все выключены, показываются все. + О приложении + Версия, сообщество, юридическая информация diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 79c73baec..0e3d4eb59 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1283,4 +1283,6 @@ Seçenekleri gizle Keşif platformları Ana akışı platforma göre filtrele. Hepsini kapalı bırakırsan her şey gösterilir. + Hakkında + Sürüm, topluluk, yasal diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index e1a6e4f3a..051f58d70 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1245,4 +1245,6 @@ 隐藏选项 发现平台 按平台筛选首页内容。全部关闭则显示全部。 + 关于 + 版本、社区、法律 From afee60d7672b14e93a4d32a215fbb522134e960e Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 18:25:38 +0500 Subject: [PATCH 158/172] tweaks: update privacy policy, mastodon, and contact email URLs --- .../tweaks/presentation/appinfo/TweaksAppInfoRoot.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt index 1e4ad9c60..f9792b8ee 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/appinfo/TweaksAppInfoRoot.kt @@ -75,16 +75,16 @@ import zed.rainxch.tweaks.presentation.TweaksAction import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.components.TweaksSubScreenScaffold -private const val PRIVACY_POLICY_URL = "https://github-store.org/privacy" +private const val PRIVACY_POLICY_URL = "https://github-store.org/privacy-policy" private const val SOURCE_CODE_URL = "https://github.com/OpenHub-Store/GitHub-Store" private const val TELEGRAM_URL = "https://t.me/githubstore" private const val DISCORD_URL = "https://discord.gg/githubstore" -private const val MASTODON_URL = "https://fosstodon.org/@githubstore" +private const val MASTODON_URL = "https://mastodon.social/@githubstore" private const val REDDIT_URL = "https://reddit.com/r/githubstore" private const val GITHUB_ORG_URL = "https://github.com/OpenHub-Store" private const val WEBSITE_URL = "https://github-store.org" -private const val BUSINESS_EMAIL = "mailto:contact@github-store.org" +private const val BUSINESS_EMAIL = "mailto:hello@github-store.org" @Composable fun TweaksAppInfoRoot( From 5f854987546150d18a13bf7eda5fb64e3db93e37 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:37:38 +0500 Subject: [PATCH 159/172] feat(devprofile): hero card, identity rail, contribution chart, clickable mentions --- .../app/navigation/AppNavigation.kt | 5 + .../devprofile/data/dto/GitHubUserResponse.kt | 1 + .../data/mappers/GitHubUserToDomain.kt | 1 + .../domain/model/DeveloperProfile.kt | 5 +- .../dev-profile/presentation/build.gradle.kts | 2 + .../presentation/DeveloperProfileAction.kt | 4 + .../presentation/DeveloperProfileRoot.kt | 32 +- .../presentation/DeveloperProfileViewModel.kt | 1 + .../components/ContributionCalendar.kt | 63 ++++ .../components/ProfileInfoCard.kt | 299 ++++++++++++------ 10 files changed, 322 insertions(+), 91 deletions(-) create mode 100644 feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 3a1856b96..b9b9de899 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -470,6 +470,11 @@ fun AppNavigation( ), ) }, + onNavigateToUser = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, viewModel = koinViewModel { parametersOf(args.username) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubUserResponse.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubUserResponse.kt index c545e459c..918f06187 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubUserResponse.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/GitHubUserResponse.kt @@ -21,4 +21,5 @@ data class GitHubUserResponse( @SerialName("created_at") val createdAt: String, @SerialName("updated_at") val updatedAt: String, @SerialName("html_url") val htmlUrl: String, + val type: String? = null, ) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubUserToDomain.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubUserToDomain.kt index c83a07f77..0c984501c 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubUserToDomain.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubUserToDomain.kt @@ -21,4 +21,5 @@ fun GitHubUserResponse.toDomain() = createdAt = createdAt, updatedAt = updatedAt, htmlUrl = htmlUrl, + userType = type, ) diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperProfile.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperProfile.kt index 4809c71f2..35e9bb522 100644 --- a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperProfile.kt +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperProfile.kt @@ -17,4 +17,7 @@ data class DeveloperProfile( val createdAt: String, val updatedAt: String, val htmlUrl: String, -) + val userType: String? = null, +) { + val isOrganization: Boolean get() = userType.equals("Organization", ignoreCase = true) +} diff --git a/feature/dev-profile/presentation/build.gradle.kts b/feature/dev-profile/presentation/build.gradle.kts index d93d82d04..94e1650b1 100644 --- a/feature/dev-profile/presentation/build.gradle.kts +++ b/feature/dev-profile/presentation/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { implementation(projects.feature.devProfile.domain) implementation(libs.bundles.landscapist) + implementation(libs.coil3.compose) + implementation(libs.coil3.svg) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.jetbrains.compose.components.resources) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileAction.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileAction.kt index 4b1a8a4b8..f0af894c7 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileAction.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileAction.kt @@ -34,4 +34,8 @@ sealed interface DeveloperProfileAction { data class OnOpenLink( val url: String, ) : DeveloperProfileAction + + data class OnNavigateToUser( + val username: String, + ) : DeveloperProfileAction } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt index aa2e01b20..6059f0b5d 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt @@ -35,6 +35,7 @@ import zed.rainxch.core.presentation.components.buttons.GhsButtonVariant import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler @@ -49,16 +50,18 @@ import zed.rainxch.core.presentation.components.ScrollbarContainer import zed.rainxch.core.presentation.locals.LocalScrollbarEnabled import zed.rainxch.core.presentation.utils.arrowKeyScroll import zed.rainxch.devprofile.domain.model.RepoFilterType +import zed.rainxch.devprofile.presentation.components.ContributionCalendarCard import zed.rainxch.devprofile.presentation.components.DeveloperRepoItem import zed.rainxch.devprofile.presentation.components.FilterSortControls +import zed.rainxch.devprofile.presentation.components.IdentityRailCard import zed.rainxch.devprofile.presentation.components.ProfileInfoCard -import zed.rainxch.devprofile.presentation.components.StatsRow import zed.rainxch.githubstore.core.presentation.res.* @Composable fun DeveloperProfileRoot( onNavigateBack: () -> Unit, onNavigateToDetails: (repoId: Long) -> Unit, + onNavigateToUser: (username: String) -> Unit, viewModel: DeveloperProfileViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -82,6 +85,13 @@ fun DeveloperProfileRoot( if (allowed) uriHandler.openUri(url) } + is DeveloperProfileAction.OnNavigateToUser -> { + val username = action.username.trim().removePrefix("@") + if (username.isNotBlank() && username != state.username) { + onNavigateToUser(username) + } + } + else -> { viewModel.onAction(action) } @@ -148,7 +158,25 @@ fun DeveloperProfileScreen( } item { - StatsRow(profile = state.profile) + val primary = MaterialTheme.colorScheme.primary + val hex = remember(primary) { + val r = (primary.red * 255).toInt().coerceIn(0, 255) + val g = (primary.green * 255).toInt().coerceIn(0, 255) + val b = (primary.blue * 255).toInt().coerceIn(0, 255) + fun byte(n: Int) = n.toString(16).padStart(2, '0') + "${byte(r)}${byte(g)}${byte(b)}" + } + ContributionCalendarCard( + username = state.profile.login, + accentHex = hex, + ) + } + + item { + IdentityRailCard( + profile = state.profile, + onAction = onAction, + ) } item { diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt index 557cb5b7a..e3887bae8 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt @@ -179,6 +179,7 @@ class DeveloperProfileViewModel( DeveloperProfileAction.OnNavigateBackClick, is DeveloperProfileAction.OnRepositoryClick, is DeveloperProfileAction.OnOpenLink, + is DeveloperProfileAction.OnNavigateToUser, -> { } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt new file mode 100644 index 000000000..280569031 --- /dev/null +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt @@ -0,0 +1,63 @@ +package zed.rainxch.devprofile.presentation.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import zed.rainxch.core.presentation.theme.tokens.Radii + +@Composable +fun ContributionCalendarCard( + username: String, + accentHex: String, + modifier: Modifier = Modifier, +) { + val url = "https://ghchart.rshah.org/${accentHex.lowercase()}/$username" + Surface( + modifier = modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Contribution activity", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.height(12.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 96.dp) + .horizontalScroll(rememberScrollState()), + contentAlignment = Alignment.CenterStart, + ) { + AsyncImage( + model = url, + contentDescription = "GitHub contribution chart for $username", + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 96.dp), + ) + } + } + } +} diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt index 23a79c937..ea8f248a7 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ProfileInfoCard.kt @@ -2,6 +2,7 @@ package zed.rainxch.devprofile.presentation.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -26,22 +27,31 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.coil3.CoilImage import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.core.presentation.utils.formatCount import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.devprofile.presentation.DeveloperProfileAction -@OptIn(ExperimentalLayoutApi::class) +private val MentionRegex = Regex("(? - Spacer(Modifier.height(6.dp)) - InfoChip(icon = Icons.Default.LocationOn, text = location) - } } } + profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> - Spacer(Modifier.height(10.dp)) - Text( - text = bio, - maxLines = 4, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Spacer(Modifier.height(12.dp)) + BioText(bio = bio, onMention = { user -> + onAction(DeveloperProfileAction.OnNavigateToUser(user)) + }) } - val hasMetaChips = profile.company != null || - profile.blog?.isNotBlank() == true || - profile.twitterUsername != null - if (hasMetaChips) { - Spacer(Modifier.height(12.dp)) - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + Spacer(Modifier.height(16.dp)) + MetricsStrip( + repos = profile.publicRepos, + followers = profile.followers, + following = profile.following, + ) + } + } +} + +@Composable +private fun BioText(bio: String, onMention: (String) -> Unit) { + val cs = MaterialTheme.colorScheme + val annotated = remember(bio, cs.primary) { + buildAnnotatedString { + var cursor = 0 + for (match in MentionRegex.findAll(bio)) { + if (match.range.first > cursor) { + append(bio.substring(cursor, match.range.first)) + } + val handle = match.groupValues[1] + withLink( + LinkAnnotation.Clickable( + tag = "mention:$handle", + styles = TextLinkStyles( + style = SpanStyle( + color = cs.primary, + fontWeight = FontWeight.SemiBold, + ), + ), + linkInteractionListener = { onMention(handle) }, + ), ) { - profile.company?.let { company -> - StaticChip(icon = Icons.Default.Business, text = company) - } - profile.blog?.takeIf { it.isNotBlank() }?.let { blog -> - val displayUrl = blog.removePrefix("https://").removePrefix("http://") - ClickableChip( - icon = Icons.Default.Link, - text = displayUrl, - onClick = { - val url = if (!blog.startsWith("http")) "https://$blog" else blog - onAction(DeveloperProfileAction.OnOpenLink(url)) - }, - ) - } - profile.twitterUsername?.let { twitter -> - ClickableChip( - icon = Icons.Default.Tag, - text = "@$twitter", - onClick = { - onAction(DeveloperProfileAction.OnOpenLink("https://twitter.com/$twitter")) - }, - ) - } + append("@$handle") } + cursor = match.range.last + 1 } + if (cursor < bio.length) append(bio.substring(cursor)) } } + Text( + text = annotated, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 5, + overflow = TextOverflow.Ellipsis, + ) } @Composable -private fun InfoChip( - icon: ImageVector, - text: String, -) { +private fun MetricsStrip(repos: Int, followers: Int, following: Int) { Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Metric(value = formatCount(repos), label = "Repos", modifier = Modifier.weight(1f)) + MetricDivider() + Metric(value = formatCount(followers), label = "Followers", modifier = Modifier.weight(1f)) + MetricDivider() + Metric(value = formatCount(following), label = "Following", modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun Metric(value: String, label: String, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { Text( - text = text, + text = value, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ), + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, + ) + Text( + text = label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, ) } } +@Composable +private fun MetricDivider() { + Box( + modifier = Modifier + .width(1.dp) + .height(28.dp) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)), + ) +} + +@Composable +private fun OrgPill() { + Surface( + shape = RoundedCornerShape(50), + color = MaterialTheme.colorScheme.tertiaryContainer, + ) { + Text( + text = "Org", + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun IdentityRailCard( + profile: DeveloperProfile, + onAction: (DeveloperProfileAction) -> Unit, +) { + val hasAny = profile.company?.isNotBlank() == true || + profile.location?.isNotBlank() == true || + profile.blog?.isNotBlank() == true || + profile.twitterUsername?.isNotBlank() == true + if (!hasAny) return + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = Radii.row, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + profile.company?.takeIf { it.isNotBlank() }?.let { company -> + val trimmed = company.trim() + if (trimmed.startsWith("@") && trimmed.length > 1) { + val handle = trimmed.removePrefix("@") + LinkChip( + icon = Icons.Default.Business, + text = trimmed, + onClick = { onAction(DeveloperProfileAction.OnNavigateToUser(handle)) }, + ) + } else { + StaticChip(icon = Icons.Default.Business, text = trimmed) + } + } + profile.location?.takeIf { it.isNotBlank() }?.let { location -> + StaticChip(icon = Icons.Default.LocationOn, text = location) + } + profile.blog?.takeIf { it.isNotBlank() }?.let { blog -> + val display = blog.removePrefix("https://").removePrefix("http://") + LinkChip( + icon = Icons.Default.Link, + text = display, + onClick = { + val url = if (!blog.startsWith("http")) "https://$blog" else blog + onAction(DeveloperProfileAction.OnOpenLink(url)) + }, + ) + } + profile.twitterUsername?.takeIf { it.isNotBlank() }?.let { twitter -> + LinkChip( + icon = Icons.Default.Tag, + text = "@$twitter", + onClick = { + onAction(DeveloperProfileAction.OnOpenLink("https://twitter.com/$twitter")) + }, + ) + } + } + } +} + @Composable private fun StaticChip(icon: ImageVector, text: String) { Surface( shape = RoundedCornerShape(50), color = MaterialTheme.colorScheme.surfaceContainerHigh, - border = BorderStroke( - width = 0.5.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), - ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(13.dp), + modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) Text( @@ -199,25 +325,22 @@ private fun StaticChip(icon: ImageVector, text: String) { } @Composable -private fun ClickableChip(icon: ImageVector, text: String, onClick: () -> Unit) { +private fun LinkChip(icon: ImageVector, text: String, onClick: () -> Unit) { Surface( - onClick = onClick, + modifier = Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(50), color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), - border = BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), - ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)), ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(13.dp), + modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.primary, ) Text( From 3f7ff9ac56ad5db1143b12bee4e24dc8e12ee277 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:42:20 +0500 Subject: [PATCH 160/172] feat(devprofile): native compose contribution calendar --- .../data/dto/ContributionsResponse.kt | 16 ++ .../DeveloperProfileRepositoryImpl.kt | 32 +++ .../domain/model/ContributionCalendar.kt | 12 ++ .../repository/DeveloperProfileRepository.kt | 3 + .../presentation/DeveloperProfileRoot.kt | 12 +- .../presentation/DeveloperProfileState.kt | 3 + .../presentation/DeveloperProfileViewModel.kt | 19 ++ .../components/ContributionCalendar.kt | 196 +++++++++++++++--- 8 files changed, 258 insertions(+), 35 deletions(-) create mode 100644 feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/ContributionsResponse.kt create mode 100644 feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/ContributionCalendar.kt diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/ContributionsResponse.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/ContributionsResponse.kt new file mode 100644 index 000000000..d7a085e5d --- /dev/null +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/dto/ContributionsResponse.kt @@ -0,0 +1,16 @@ +package zed.rainxch.devprofile.data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ContributionsResponse( + val total: Map = emptyMap(), + val contributions: List = emptyList(), +) + +@Serializable +data class ContributionDayResponse( + val date: String, + val count: Int, + val level: Int, +) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt index 71c5f76b4..9bd1a0a10 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt @@ -24,11 +24,16 @@ import zed.rainxch.core.domain.logging.GitHubStoreLogger import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.model.RateLimitException import zed.rainxch.core.domain.repository.FavouritesRepository +import io.ktor.client.request.headers +import io.ktor.client.request.url +import zed.rainxch.devprofile.data.dto.ContributionsResponse import zed.rainxch.devprofile.data.dto.GitHubRepoResponse import zed.rainxch.devprofile.data.dto.GitHubUserResponse import zed.rainxch.devprofile.data.mappers.toDeveloperProfile import zed.rainxch.devprofile.data.mappers.toDomain import zed.rainxch.devprofile.data.mappers.toGitHubRepoResponse +import zed.rainxch.devprofile.domain.model.ContributionCalendar +import zed.rainxch.devprofile.domain.model.ContributionDay import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.devprofile.domain.model.DeveloperRepository import zed.rainxch.devprofile.domain.repository.DeveloperProfileRepository @@ -75,6 +80,33 @@ class DeveloperProfileRepositoryImpl( } } + override suspend fun getContributionCalendar(username: String): Result { + return withContext(Dispatchers.IO) { + try { + val response = httpClient.get { + url("https://github-contributions-api.jogruber.de/v4/$username?y=last") + headers { remove("X-GitHub-Token") } + } + if (!response.status.isSuccess()) { + return@withContext Result.failure( + Exception("Failed to fetch contributions: ${response.status.description}"), + ) + } + val body: ContributionsResponse = response.body() + val days = body.contributions.map { + ContributionDay(date = it.date, count = it.count, level = it.level) + } + val total = body.total["lastYear"] ?: body.total.values.firstOrNull() ?: 0 + Result.success(ContributionCalendar(totalLastYear = total, days = days)) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logger.warn("Failed to fetch contributions for $username: ${e.message}") + Result.failure(e) + } + } + } + override suspend fun getDeveloperRepositories(username: String): Result> { return withContext(Dispatchers.IO) { try { diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/ContributionCalendar.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/ContributionCalendar.kt new file mode 100644 index 000000000..12ecb0e43 --- /dev/null +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/ContributionCalendar.kt @@ -0,0 +1,12 @@ +package zed.rainxch.devprofile.domain.model + +data class ContributionDay( + val date: String, + val count: Int, + val level: Int, +) + +data class ContributionCalendar( + val totalLastYear: Int, + val days: List, +) diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/repository/DeveloperProfileRepository.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/repository/DeveloperProfileRepository.kt index fac1769d2..d1483b06c 100644 --- a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/repository/DeveloperProfileRepository.kt +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/repository/DeveloperProfileRepository.kt @@ -1,5 +1,6 @@ package zed.rainxch.devprofile.domain.repository +import zed.rainxch.devprofile.domain.model.ContributionCalendar import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.devprofile.domain.model.DeveloperRepository @@ -7,4 +8,6 @@ interface DeveloperProfileRepository { suspend fun getDeveloperProfile(username: String): Result suspend fun getDeveloperRepositories(username: String): Result> + + suspend fun getContributionCalendar(username: String): Result } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt index 6059f0b5d..8d0dfa481 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt @@ -158,17 +158,9 @@ fun DeveloperProfileScreen( } item { - val primary = MaterialTheme.colorScheme.primary - val hex = remember(primary) { - val r = (primary.red * 255).toInt().coerceIn(0, 255) - val g = (primary.green * 255).toInt().coerceIn(0, 255) - val b = (primary.blue * 255).toInt().coerceIn(0, 255) - fun byte(n: Int) = n.toString(16).padStart(2, '0') - "${byte(r)}${byte(g)}${byte(b)}" - } ContributionCalendarCard( - username = state.profile.login, - accentHex = hex, + contributions = state.contributions, + isLoading = state.isLoadingContributions, ) } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileState.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileState.kt index bb01bf261..f1e753931 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileState.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileState.kt @@ -2,6 +2,7 @@ package zed.rainxch.devprofile.presentation import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import zed.rainxch.devprofile.domain.model.ContributionCalendar import zed.rainxch.devprofile.domain.model.DeveloperProfile import zed.rainxch.devprofile.domain.model.DeveloperRepository import zed.rainxch.devprofile.domain.model.RepoFilterType @@ -18,4 +19,6 @@ data class DeveloperProfileState( val currentFilter: RepoFilterType = RepoFilterType.WITH_RELEASES, val currentSort: RepoSortType = RepoSortType.UPDATED, val searchQuery: String = "", + val contributions: ContributionCalendar? = null, + val isLoadingContributions: Boolean = false, ) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt index e3887bae8..77ec255c4 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt @@ -64,8 +64,10 @@ class DeveloperProfileViewModel( profile = profile, isLoading = false, isLoadingRepos = true, + isLoadingContributions = true, ) } + loadContributions() }.onFailure { error -> _state.update { it.copy( @@ -119,6 +121,23 @@ class DeveloperProfileViewModel( } } + private fun loadContributions() { + viewModelScope.launch { + repository.getContributionCalendar(username) + .onSuccess { cal -> + _state.update { + it.copy( + contributions = cal, + isLoadingContributions = false, + ) + } + } + .onFailure { + _state.update { it.copy(isLoadingContributions = false) } + } + } + } + private fun applyFiltersAndSort() { viewModelScope.launch(Dispatchers.Default) { val currentState = _state.value diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt index 280569031..493047246 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/ContributionCalendar.kt @@ -1,33 +1,48 @@ package zed.rainxch.devprofile.presentation.components import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import zed.rainxch.core.presentation.theme.tokens.Radii +import zed.rainxch.devprofile.domain.model.ContributionCalendar +import zed.rainxch.devprofile.domain.model.ContributionDay + +private val CELL_SIZE = 11.dp +private val CELL_GAP = 3.dp @Composable fun ContributionCalendarCard( - username: String, - accentHex: String, + contributions: ContributionCalendar?, + isLoading: Boolean, modifier: Modifier = Modifier, ) { - val url = "https://ghchart.rshah.org/${accentHex.lowercase()}/$username" Surface( modifier = modifier.fillMaxWidth(), shape = Radii.row, @@ -35,29 +50,160 @@ fun ContributionCalendarCard( border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), ) { Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Contribution activity", - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(Modifier.height(12.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 96.dp) - .horizontalScroll(rememberScrollState()), - contentAlignment = Alignment.CenterStart, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - AsyncImage( - model = url, - contentDescription = "GitHub contribution chart for $username", - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 96.dp), + Text( + text = "Contribution activity", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurface, ) + contributions?.let { + Text( + text = "${it.totalLastYear} this year", + style = MaterialTheme.typography.labelSmall.copy( + fontFamily = FontFamily.Monospace, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.height(12.dp)) + when { + isLoading && contributions == null -> { + Text( + text = "Loading…", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + contributions == null -> { + Text( + text = "Couldn't load contributions.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + else -> CalendarGrid(days = contributions.days) } } } } + +@Composable +private fun CalendarGrid(days: List) { + if (days.isEmpty()) return + val cs = MaterialTheme.colorScheme + val baseTint = remember(cs.surfaceContainerHigh) { cs.surfaceContainerHigh } + val primary = cs.primary + val palette = remember(primary, baseTint) { + listOf( + baseTint, + primary.copy(alpha = 0.25f).compositeOver(baseTint), + primary.copy(alpha = 0.5f).compositeOver(baseTint), + primary.copy(alpha = 0.75f).compositeOver(baseTint), + primary, + ) + } + val scrollState = rememberScrollState() + + val firstDayOfWeek = remember(days) { + val first = days.firstOrNull()?.date ?: return@remember 0 + dateToDayOfWeek(first) + } + val totalCells = firstDayOfWeek + days.size + val weeks = (totalCells + 6) / 7 + + Box( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(scrollState), + ) { + Canvas( + modifier = Modifier.size( + width = (CELL_SIZE + CELL_GAP) * weeks, + height = (CELL_SIZE + CELL_GAP) * 7, + ), + ) { + val cellPx = CELL_SIZE.toPx() + val gapPx = CELL_GAP.toPx() + val stride = cellPx + gapPx + val radius = CornerRadius(cellPx * 0.18f, cellPx * 0.18f) + + for (i in days.indices) { + val cellIndex = firstDayOfWeek + i + val week = cellIndex / 7 + val dow = cellIndex % 7 + val day = days[i] + val color = palette[day.level.coerceIn(0, 4)] + drawRoundRect( + color = color, + topLeft = Offset(week * stride, dow * stride), + size = Size(cellPx, cellPx), + cornerRadius = radius, + ) + } + } + } + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Less", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + palette.forEach { c -> + Box( + modifier = Modifier + .size(CELL_SIZE) + .clip(RoundedCornerShape(2.dp)) + .background(c), + ) + } + Text( + text = "More", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +private fun dateToDayOfWeek(iso: String): Int { + val parts = iso.split('-') + if (parts.size != 3) return 0 + val y = parts[0].toIntOrNull() ?: return 0 + val m = parts[1].toIntOrNull() ?: return 0 + val d = parts[2].toIntOrNull() ?: return 0 + return zellerDayOfWeek(y, m, d) +} + +private fun zellerDayOfWeek(year: Int, month: Int, day: Int): Int { + var y = year + var m = month + if (m < 3) { + m += 12 + y -= 1 + } + val k = y % 100 + val j = y / 100 + val h = (day + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7 + return ((h + 6) % 7) +} + +private fun Color.compositeOver(background: Color): Color { + val a = alpha + val invA = 1f - a + return Color( + red = red * a + background.red * invA, + green = green * a + background.green * invA, + blue = blue * a + background.blue * invA, + alpha = 1f, + ) +} From 01bda3e478bc324b82659806d365398decb57667 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:50:19 +0500 Subject: [PATCH 161/172] feat(devprofile): split has-releases vs installable filters, DS pill chips, redesigned card badges --- .../composeResources/values/strings.xml | 6 +- .../devprofile/domain/model/RepoFilterType.kt | 1 + .../presentation/DeveloperProfileRoot.kt | 1 + .../presentation/DeveloperProfileViewModel.kt | 8 +- .../components/DeveloperRepoItem.kt | 11 +- .../components/FilterSortControls.kt | 235 +++++++++--------- 6 files changed, 137 insertions(+), 125 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 91c19c575..7f3a18733 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -388,7 +388,8 @@ Search repositories… Clear search - With Releases + With releases + Installable here Installed Favorites Sort @@ -402,7 +403,8 @@ Showing %1$d of %2$d repositories - No repositories with installable releases + No repositories with releases + No repositories with installable assets for this platform No installed repositories No favorite repositories diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoFilterType.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoFilterType.kt index ae0232d34..cbc2bc1e5 100644 --- a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoFilterType.kt +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/RepoFilterType.kt @@ -2,6 +2,7 @@ package zed.rainxch.devprofile.domain.model enum class RepoFilterType { WITH_RELEASES, + WITH_INSTALLABLE, INSTALLED, FAVORITES, } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt index 8d0dfa481..e58565d52 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt @@ -272,6 +272,7 @@ private fun EmptyReposContent( val message = when (filter) { RepoFilterType.WITH_RELEASES -> stringResource(Res.string.no_repos_with_releases) + RepoFilterType.WITH_INSTALLABLE -> stringResource(Res.string.no_repos_with_installable) RepoFilterType.INSTALLED -> stringResource(Res.string.no_installed_repos) RepoFilterType.FAVORITES -> stringResource(Res.string.no_favorite_repos) } diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt index 77ec255c4..affb15851 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt @@ -156,9 +156,11 @@ class DeveloperProfileViewModel( filtered = when (currentState.currentFilter) { RepoFilterType.WITH_RELEASES -> { - filtered - .filter { it.hasInstallableAssets } - .toImmutableList() + filtered.filter { it.hasReleases }.toImmutableList() + } + + RepoFilterType.WITH_INSTALLABLE -> { + filtered.filter { it.hasInstallableAssets }.toImmutableList() } RepoFilterType.INSTALLED -> { diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt index 8f8098904..80798adf5 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt @@ -174,7 +174,9 @@ fun DeveloperRepoItem( } } - val showBadges = repository.hasInstallableAssets || repository.isInstalled + val showBadges = repository.hasReleases || + repository.hasInstallableAssets || + repository.isInstalled if (showBadges) { Spacer(Modifier.height(10.dp)) FlowRow( @@ -189,6 +191,13 @@ fun DeveloperRepoItem( container = MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), content = MaterialTheme.colorScheme.primary, ) + } else if (repository.hasReleases) { + TonalBadge( + text = repository.latestVersion + ?: stringResource(Res.string.has_release), + container = MaterialTheme.colorScheme.secondaryContainer, + content = MaterialTheme.colorScheme.onSecondaryContainer, + ) } if (repository.isInstalled) { TonalBadge( diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt index 03ee5bffb..fe25f23cd 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/FilterSortControls.kt @@ -1,32 +1,27 @@ package zed.rainxch.devprofile.presentation.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SecondaryScrollableTabRow -import androidx.compose.material3.Tab import androidx.compose.material3.Text -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -34,15 +29,32 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.core.presentation.components.inputs.GhsTextField +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenu +import zed.rainxch.core.presentation.components.overlays.GhsDropdownMenuItem +import zed.rainxch.core.presentation.theme.tokens.Radii import zed.rainxch.devprofile.domain.model.RepoFilterType import zed.rainxch.devprofile.domain.model.RepoSortType import zed.rainxch.devprofile.presentation.DeveloperProfileAction -import zed.rainxch.githubstore.core.presentation.res.* +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.clear_search +import zed.rainxch.githubstore.core.presentation.res.filter_favorites +import zed.rainxch.githubstore.core.presentation.res.filter_installed +import zed.rainxch.githubstore.core.presentation.res.filter_with_installable +import zed.rainxch.githubstore.core.presentation.res.filter_with_releases +import zed.rainxch.githubstore.core.presentation.res.repositories +import zed.rainxch.githubstore.core.presentation.res.repository_singular +import zed.rainxch.githubstore.core.presentation.res.search_repositories +import zed.rainxch.githubstore.core.presentation.res.showing_x_of_y_repositories +import zed.rainxch.githubstore.core.presentation.res.sort +import zed.rainxch.githubstore.core.presentation.res.sort_most_stars +import zed.rainxch.githubstore.core.presentation.res.sort_name +import zed.rainxch.githubstore.core.presentation.res.sort_recently_updated -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FilterSortControls( currentFilter: RepoFilterType, @@ -58,17 +70,13 @@ fun FilterSortControls( ) { GhsTextField( value = searchQuery, - onValueChange = { query -> - onAction(DeveloperProfileAction.OnSearchQueryChange(query)) - }, + onValueChange = { onAction(DeveloperProfileAction.OnSearchQueryChange(it)) }, modifier = Modifier.fillMaxWidth(), placeholder = stringResource(Res.string.search_repositories), leadingIcon = Icons.Default.Search, trailingIcon = { if (searchQuery.isNotBlank()) { - IconButton( - onClick = { onAction(DeveloperProfileAction.OnSearchQueryChange("")) }, - ) { + IconButton(onClick = { onAction(DeveloperProfileAction.OnSearchQueryChange("")) }) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(Res.string.clear_search), @@ -82,143 +90,133 @@ fun FilterSortControls( Row( modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - SecondaryScrollableTabRow( - selectedTabIndex = currentFilter.ordinal, - modifier = Modifier.weight(1f), - edgePadding = 0.dp, - divider = {}, + Row( + modifier = Modifier + .weight(1f) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { RepoFilterType.entries.forEach { filter -> - FilterChipTab( - selected = currentFilter == filter, - onClick = { onAction(DeveloperProfileAction.OnFilterChange(filter)) }, + FilterPill( label = filter.displayName(), + isSelected = currentFilter == filter, + onClick = { onAction(DeveloperProfileAction.OnFilterChange(filter)) }, ) } } - - SortMenu( + SortButton( currentSort = currentSort, - onSortChange = { sort -> - onAction(DeveloperProfileAction.OnSortChange(sort)) - }, + onSortChange = { onAction(DeveloperProfileAction.OnSortChange(it)) }, ) } Text( - text = - if (repoCount == totalCount) { - "$repoCount ${ - stringResource( - if (repoCount == 1) { - Res.string.repository_singular - } else { - Res.string.repositories - }, - ) - }" - } else { - stringResource( - resource = Res.string.showing_x_of_y_repositories, - repoCount, - totalCount, - ) - }, + text = if (repoCount == totalCount) { + "$repoCount ${stringResource( + if (repoCount == 1) Res.string.repository_singular else Res.string.repositories, + )}" + } else { + stringResource(Res.string.showing_x_of_y_repositories, repoCount, totalCount) + }, maxLines = 1, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @Composable -private fun FilterChipTab( - selected: Boolean, - onClick: () -> Unit, +private fun FilterPill( label: String, + isSelected: Boolean, + onClick: () -> Unit, ) { - Tab( - selected = selected, - onClick = onClick, - modifier = Modifier.height(40.dp), + val container by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + animationSpec = tween(durationMillis = 180), + label = "filter_container", + ) + val content by animateColorAsState( + targetValue = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + animationSpec = tween(durationMillis = 180), + label = "filter_content", + ) + Box( + modifier = Modifier + .clip(Radii.chip) + .background(container) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 10.dp), + contentAlignment = Alignment.Center, ) { Text( text = label, - style = MaterialTheme.typography.labelMedium, - color = - if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + ), + color = content, + maxLines = 1, ) } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun SortMenu( +private fun SortButton( currentSort: RepoSortType, onSortChange: (RepoSortType) -> Unit, ) { var expanded by remember { mutableStateOf(false) } - Box { - FilledIconButton( - onClick = { expanded = true }, - modifier = Modifier.size(40.dp), - shape = MaterialShapes.Cookie9Sided.toShape(), + Box( + modifier = Modifier + .size(40.dp) + .clip(Radii.chip) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .clickable { expanded = true }, + contentAlignment = Alignment.Center, ) { Icon( imageVector = Icons.AutoMirrored.Filled.Sort, contentDescription = stringResource(Res.string.sort), - modifier = Modifier.size(20.dp), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface, ) } - - DropdownMenu( + GhsDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, - shape = RoundedCornerShape(32.dp), ) { RepoSortType.entries.forEach { sort -> - DropdownMenuItem( - text = { - Row( - modifier = Modifier.padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (currentSort == sort) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } else { - Spacer(modifier = Modifier.size(18.dp)) - } - - Text( - text = sort.displayName(), - maxLines = 1, - style = MaterialTheme.typography.bodyMedium, - color = - if (currentSort == sort) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, - ) - } - }, + GhsDropdownMenuItem( + text = sort.displayName(), onClick = { onSortChange(sort) expanded = false }, + trailingIcon = if (currentSort == sort) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + } else { + null + }, ) } } @@ -226,17 +224,16 @@ private fun SortMenu( } @Composable -private fun RepoFilterType.displayName(): String = - when (this) { - RepoFilterType.WITH_RELEASES -> stringResource(Res.string.filter_with_releases) - RepoFilterType.INSTALLED -> stringResource(Res.string.filter_installed) - RepoFilterType.FAVORITES -> stringResource(Res.string.filter_favorites) - } +private fun RepoFilterType.displayName(): String = when (this) { + RepoFilterType.WITH_RELEASES -> stringResource(Res.string.filter_with_releases) + RepoFilterType.WITH_INSTALLABLE -> stringResource(Res.string.filter_with_installable) + RepoFilterType.INSTALLED -> stringResource(Res.string.filter_installed) + RepoFilterType.FAVORITES -> stringResource(Res.string.filter_favorites) +} @Composable -private fun RepoSortType.displayName(): String = - when (this) { - RepoSortType.UPDATED -> stringResource(Res.string.sort_recently_updated) - RepoSortType.STARS -> stringResource(Res.string.sort_most_stars) - RepoSortType.NAME -> stringResource(Res.string.sort_name) - } +private fun RepoSortType.displayName(): String = when (this) { + RepoSortType.UPDATED -> stringResource(Res.string.sort_recently_updated) + RepoSortType.STARS -> stringResource(Res.string.sort_most_stars) + RepoSortType.NAME -> stringResource(Res.string.sort_name) +} From 660350fa0ca509aa2283de4b428e5db9abc917f6 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:50:20 +0500 Subject: [PATCH 162/172] i18n(devprofile): translate filter strings to 12 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 2 ++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../src/commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 2 ++ .../src/commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 2 ++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 2 ++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ 12 files changed, 24 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index cd6467389..1f2fef9ba 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1313,4 +1313,6 @@ تصفية الصفحة الرئيسية حسب المنصة. اترك الكل بدون تحديد لعرض كل شيء. حول الإصدار، المجتمع، القانوني + قابل للتثبيت هنا + لا توجد مستودعات بأصول قابلة للتثبيت لهذه المنصة diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index ba04406fa..fa5b8c1ec 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1275,4 +1275,6 @@ হোম ফিডকে নির্দিষ্ট প্ল্যাটফর্মে ফিল্টার করুন। সব বন্ধ রাখলে সব দেখাবে। সম্পর্কে সংস্করণ, কমিউনিটি, আইনি + এখানে ইনস্টলযোগ্য + এই প্ল্যাটফর্মের জন্য ইনস্টলযোগ্য অ্যাসেট সহ কোনো রিপো নেই diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 70995cf67..c8d739408 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1254,4 +1254,6 @@ Filtra el feed principal por plataforma. Déjalas todas desactivadas para verlo todo. Acerca de Versión, comunidad, legal + Instalable aquí + Sin repositorios con assets instalables para esta plataforma diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 730291a7d..736ed43f2 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1254,4 +1254,6 @@ Filtre le fil d'accueil par plateforme. Laisse tout désactivé pour tout voir. À propos Version, communauté, mentions légales + Installable ici + Aucun dépôt avec des assets installables pour cette plateforme diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index e6881e082..edbdee6f9 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1290,4 +1290,6 @@ होम फ़ीड को विशिष्ट प्लेटफ़ॉर्म पर फ़िल्टर करें। सब बंद रहने पर सब कुछ दिखेगा। के बारे में संस्करण, समुदाय, कानूनी + यहाँ इंस्टॉल योग्य + इस प्लेटफ़ॉर्म के लिए इंस्टॉल योग्य ऐसेट वाले रिपॉज़िटरी नहीं diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index a6b053696..8ccb38a10 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1288,4 +1288,6 @@ Filtra il feed Home per piattaforma. Lasciale tutte disattivate per vedere tutto. Informazioni Versione, community, legale + Installabile qui + Nessun repository con asset installabili per questa piattaforma diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index b0dde97b6..851d2558c 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1244,4 +1244,6 @@ ホームフィードを特定のプラットフォームで絞り込みます。すべてオフで全表示。 アプリ情報 バージョン、コミュニティ、法的情報 + ここでインストール可能 + このプラットフォーム向けのインストール可能なアセットを持つリポジトリはありません diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 8dc399c59..8d79f2208 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1274,4 +1274,6 @@ 홈 피드를 특정 플랫폼으로 필터링합니다. 모두 꺼두면 전체 표시. 정보 버전, 커뮤니티, 법적 고지 + 여기서 설치 가능 + 이 플랫폼용 설치 가능한 자산이 있는 저장소가 없습니다 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index c15dfa0cf..d931d74cc 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1283,4 +1283,6 @@ Filtruj kanał Home według platformy. Pozostaw wszystkie wyłączone, aby zobaczyć wszystko. Informacje Wersja, społeczność, informacje prawne + Instalowalne tutaj + Brak repozytoriów z plikami instalowalnymi dla tej platformy diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 10920f5c6..8419bda60 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1283,4 +1283,6 @@ Фильтрация главной ленты по платформам. Если все выключены, показываются все. О приложении Версия, сообщество, юридическая информация + Можно установить здесь + Нет репозиториев с устанавливаемыми сборками для этой платформы diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 0e3d4eb59..5d151ece6 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1285,4 +1285,6 @@ Ana akışı platforma göre filtrele. Hepsini kapalı bırakırsan her şey gösterilir. Hakkında Sürüm, topluluk, yasal + Burada kurulabilir + Bu platform için kurulabilir varlığı olan depo yok diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 051f58d70..cca4a2fc0 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1247,4 +1247,6 @@ 按平台筛选首页内容。全部关闭则显示全部。 关于 版本、社区、法律 + 可在此安装 + 没有适用于当前平台的可安装资源仓库 From 28eee40c2b472ba844535437945ec8197fe150dc Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:51:52 +0500 Subject: [PATCH 163/172] fix(theme): tint dynamic palette surfaces toward accent --- .../rainxch/core/presentation/theme/Theme.kt | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index 76d1dac75..c0cc6f9f4 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -37,15 +37,11 @@ fun GithubStoreTheme( } val baseScheme = colorSchemeFor(palette = tokenPalette, mode = mode) val targetScheme = if (dynamic != null) { - if (isAmoledTheme && isDarkTheme) { - dynamic.copy( - background = Color.Black, - surface = Color(0xFF0B0F14), - surfaceContainerLowest = Color.Black, - ) - } else { - dynamic - } + applyDynamicSurfaces( + dynamic = dynamic, + isDark = isDarkTheme, + isAmoled = isAmoledTheme && isDarkTheme, + ) } else { baseScheme } @@ -67,3 +63,43 @@ fun GithubStoreTheme( ) } } + +private fun applyDynamicSurfaces( + dynamic: ColorScheme, + isDark: Boolean, + isAmoled: Boolean, +): ColorScheme { + val accent = dynamic.primary + val base = when { + isAmoled -> Color.Black + isDark -> Color(0xFF101319) + else -> Color.White + } + fun tint(ratio: Float): Color = blend(base, accent, ratio) + return dynamic.copy( + background = if (isAmoled) Color.Black else tint(0.06f), + surface = if (isAmoled) Color(0xFF0B0F14) else tint(if (isDark) 0.08f else 0.04f), + surfaceVariant = tint(if (isDark) 0.14f else 0.10f), + surfaceTint = accent, + surfaceBright = tint(if (isDark) 0.16f else 0.02f), + surfaceDim = tint(if (isDark) 0.04f else 0.10f), + surfaceContainerLowest = if (isAmoled) Color.Black else if (isDark) base else Color.White, + surfaceContainerLow = tint(if (isDark) 0.06f else 0.04f), + surfaceContainer = tint(if (isDark) 0.09f else 0.07f), + surfaceContainerHigh = tint(if (isDark) 0.12f else 0.10f), + surfaceContainerHighest = tint(if (isDark) 0.16f else 0.13f), + outline = tint(if (isDark) 0.30f else 0.25f), + outlineVariant = tint(if (isDark) 0.18f else 0.15f), + ) +} + +private fun blend(base: Color, accent: Color, ratio: Float): Color { + val r = ratio.coerceIn(0f, 1f) + val inv = 1f - r + return Color( + red = base.red * inv + accent.red * r, + green = base.green * inv + accent.green * r, + blue = base.blue * inv + accent.blue * r, + alpha = 1f, + ) +} From dd02ec987269214e6131365481a1bdf47c97917a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:55:02 +0500 Subject: [PATCH 164/172] fix(theme): dynamic surfaces lower than containers for contrast --- .../rainxch/core/presentation/theme/Theme.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt index c0cc6f9f4..25b669cf8 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/theme/Theme.kt @@ -77,19 +77,19 @@ private fun applyDynamicSurfaces( } fun tint(ratio: Float): Color = blend(base, accent, ratio) return dynamic.copy( - background = if (isAmoled) Color.Black else tint(0.06f), - surface = if (isAmoled) Color(0xFF0B0F14) else tint(if (isDark) 0.08f else 0.04f), - surfaceVariant = tint(if (isDark) 0.14f else 0.10f), + background = if (isAmoled) Color.Black else tint(if (isDark) 0.04f else 0.03f), + surface = if (isAmoled) Color(0xFF0B0F14) else tint(if (isDark) 0.04f else 0.03f), + surfaceVariant = tint(if (isDark) 0.15f else 0.11f), surfaceTint = accent, - surfaceBright = tint(if (isDark) 0.16f else 0.02f), - surfaceDim = tint(if (isDark) 0.04f else 0.10f), + surfaceBright = tint(if (isDark) 0.18f else 0.02f), + surfaceDim = tint(if (isDark) 0.02f else 0.10f), surfaceContainerLowest = if (isAmoled) Color.Black else if (isDark) base else Color.White, - surfaceContainerLow = tint(if (isDark) 0.06f else 0.04f), - surfaceContainer = tint(if (isDark) 0.09f else 0.07f), - surfaceContainerHigh = tint(if (isDark) 0.12f else 0.10f), - surfaceContainerHighest = tint(if (isDark) 0.16f else 0.13f), - outline = tint(if (isDark) 0.30f else 0.25f), - outlineVariant = tint(if (isDark) 0.18f else 0.15f), + surfaceContainerLow = tint(if (isDark) 0.07f else 0.06f), + surfaceContainer = tint(if (isDark) 0.10f else 0.09f), + surfaceContainerHigh = tint(if (isDark) 0.13f else 0.12f), + surfaceContainerHighest = tint(if (isDark) 0.17f else 0.15f), + outline = tint(if (isDark) 0.32f else 0.28f), + outlineVariant = tint(if (isDark) 0.20f else 0.16f), ) } From cdbb2f6e7bb705a120832dd8a240f980c22b644a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:55:44 +0500 Subject: [PATCH 165/172] fix(devprofile): hide contribution chart for orgs --- .../devprofile/presentation/DeveloperProfileRoot.kt | 12 +++++++----- .../presentation/DeveloperProfileViewModel.kt | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt index e58565d52..72ea9399c 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileRoot.kt @@ -157,11 +157,13 @@ fun DeveloperProfileScreen( ) } - item { - ContributionCalendarCard( - contributions = state.contributions, - isLoading = state.isLoadingContributions, - ) + if (!state.profile.isOrganization) { + item { + ContributionCalendarCard( + contributions = state.contributions, + isLoading = state.isLoadingContributions, + ) + } } item { diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt index affb15851..b3fbe453c 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/DeveloperProfileViewModel.kt @@ -64,10 +64,12 @@ class DeveloperProfileViewModel( profile = profile, isLoading = false, isLoadingRepos = true, - isLoadingContributions = true, + isLoadingContributions = !profile.isOrganization, ) } - loadContributions() + if (!profile.isOrganization) { + loadContributions() + } }.onFailure { error -> _state.update { it.copy( From c99dfd5185ff97f8fa0513e29fd7a4eda3dd864c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 25 May 2026 19:58:08 +0500 Subject: [PATCH 166/172] feat(devprofile): show latest release date on repo cards --- .../data/mappers/GitHubRepoToDomain.kt | 2 + .../DeveloperProfileRepositoryImpl.kt | 79 ++++++++++--------- .../domain/model/DeveloperRepository.kt | 1 + .../components/DeveloperRepoItem.kt | 12 ++- 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt index e11c04d48..f840b25b8 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/mappers/GitHubRepoToDomain.kt @@ -9,6 +9,7 @@ fun GitHubRepoResponse.toDomain( isInstalled: Boolean = false, isFavorite: Boolean = false, latestVersion: String? = null, + latestReleaseAt: String? = null, ) = DeveloperRepository( id = id, name = name, @@ -24,5 +25,6 @@ fun GitHubRepoResponse.toDomain( isInstalled = isInstalled, isFavorite = isFavorite, latestVersion = latestVersion, + latestReleaseAt = latestReleaseAt, updatedAt = updatedAt, ) diff --git a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt index 9bd1a0a10..7e2b95317 100644 --- a/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt +++ b/feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt @@ -202,31 +202,43 @@ class DeveloperProfileRepositoryImpl( val installedApps = installedAppsDao.getAppsByRepoId(repo.id) val isFavorite = favoriteIds.contains(repo.id) - val (hasReleases, hasInstallableAssets, latestVersion) = - checkReleaseInfo( - owner = repo.fullName.split("/")[0], - repoName = repo.name, - ) + val info = checkReleaseInfo( + owner = repo.fullName.split("/")[0], + repoName = repo.name, + ) return repo.toDomain( - hasReleases = hasReleases, - hasInstallableAssets = hasInstallableAssets, - + hasReleases = info.hasReleases, + hasInstallableAssets = info.hasInstallable, isInstalled = installedApps.any { !it.isPendingInstall }, isFavorite = isFavorite, - latestVersion = latestVersion, + latestVersion = info.latestVersion, + latestReleaseAt = info.publishedAt, ) } + private data class ReleaseInfo( + val hasReleases: Boolean, + val hasInstallable: Boolean, + val latestVersion: String?, + val publishedAt: String?, + ) { + companion object { + val EMPTY = ReleaseInfo(false, false, null, null) + } + } + private suspend fun checkReleaseInfo( owner: String, repoName: String, - ): Triple { + ): ReleaseInfo { val backendResult = backendApiClient.getReleases(owner, repoName, perPage = 10) backendResult.fold( onSuccess = { releases -> val stableRelease = releases.firstOrNull { it.draft != true && it.prerelease != true } - if (stableRelease == null) return Triple(releases.isNotEmpty(), false, null) + if (stableRelease == null) { + return ReleaseInfo(releases.isNotEmpty(), false, null, null) + } val hasInstallable = stableRelease.assets.any { asset -> val name = asset.name.lowercase() when (platform) { @@ -237,10 +249,15 @@ class DeveloperProfileRepositoryImpl( name.endsWith(".rpm") || name.endsWith(".pkg.tar.zst") } } - return Triple(true, hasInstallable, stableRelease.tagName) + return ReleaseInfo( + hasReleases = true, + hasInstallable = hasInstallable, + latestVersion = stableRelease.tagName, + publishedAt = stableRelease.publishedAt, + ) }, onFailure = { error -> - if (!shouldFallbackToGithubOrRethrow(error)) return Triple(false, false, null) + if (!shouldFallbackToGithubOrRethrow(error)) return ReleaseInfo.EMPTY }, ) @@ -251,7 +268,7 @@ class DeveloperProfileRepositoryImpl( } if (!response.status.isSuccess()) { - return Triple(false, false, null) + return ReleaseInfo.EMPTY } val releases: List = response.body() @@ -262,36 +279,26 @@ class DeveloperProfileRepositoryImpl( } if (stableRelease == null) { - return Triple(releases.isNotEmpty(), false, null) + return ReleaseInfo(releases.isNotEmpty(), false, null, null) } val hasInstallableAssets = stableRelease.assets.any { asset -> val name = asset.name.lowercase() when (platform) { - Platform.ANDROID -> { - name.endsWith(".apk") - } - - Platform.WINDOWS -> { - name.endsWith(".msi") || name.endsWith(".exe") - } - - Platform.MACOS -> { - name.endsWith(".dmg") || name.endsWith(".pkg") - } - - Platform.LINUX -> { - name.endsWith(".appimage") || name.endsWith(".deb") || - name.endsWith(".rpm") || name.endsWith(".pkg.tar.zst") - } + Platform.ANDROID -> name.endsWith(".apk") + Platform.WINDOWS -> name.endsWith(".msi") || name.endsWith(".exe") + Platform.MACOS -> name.endsWith(".dmg") || name.endsWith(".pkg") + Platform.LINUX -> name.endsWith(".appimage") || name.endsWith(".deb") || + name.endsWith(".rpm") || name.endsWith(".pkg.tar.zst") } } - Triple( - true, - hasInstallableAssets, - if (hasInstallableAssets) stableRelease.tagName else null, + ReleaseInfo( + hasReleases = true, + hasInstallable = hasInstallableAssets, + latestVersion = stableRelease.tagName, + publishedAt = stableRelease.publishedAt, ) } catch (e: RateLimitException) { throw e @@ -299,7 +306,7 @@ class DeveloperProfileRepositoryImpl( throw e } catch (e: Exception) { logger.warn("Failed to check releases for $owner/$repoName : ${e.message}") - Triple(false, false, null) + ReleaseInfo.EMPTY } } diff --git a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt index b610b196e..5cdb9ad7c 100644 --- a/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt +++ b/feature/dev-profile/domain/src/commonMain/kotlin/zed/rainxch/devprofile/domain/model/DeveloperRepository.kt @@ -15,5 +15,6 @@ data class DeveloperRepository( val isInstalled: Boolean = false, val isFavorite: Boolean = false, val latestVersion: String? = null, + val latestReleaseAt: String? = null, val updatedAt: String, ) diff --git a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt index 80798adf5..29c8d90b1 100644 --- a/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt +++ b/feature/dev-profile/presentation/src/commonMain/kotlin/zed/rainxch/devprofile/presentation/components/DeveloperRepoItem.kt @@ -84,11 +84,15 @@ fun DeveloperRepoItem( overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface, ) + val releaseDate = repository.latestReleaseAt + val (label, dateString) = if (releaseDate != null) { + Res.string.released_on_date to releaseDate + } else { + Res.string.updated_on_date to repository.updatedAt + } Text( - text = stringResource( - resource = Res.string.updated_on_date, - formatRelativeDate(repository.updatedAt), - ).replaceFirstChar { it.uppercase() }, + text = stringResource(label, formatRelativeDate(dateString)) + .replaceFirstChar { it.uppercase() }, style = MaterialTheme.typography.labelMedium, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurfaceVariant, From 13c8be2f417665fbf89a5c944b3a6d971b908849 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 12:39:14 +0500 Subject: [PATCH 167/172] feat(tweaks): update interval slider up to 30 days --- .../composeResources/values/strings.xml | 10 +- .../components/sections/Installation.kt | 100 ++++++++++++------ 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 7f3a18733..1a5449c1f 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -582,10 +582,12 @@ How often to look for new releases. Check automatically Look for new releases on a schedule. Turn off to save battery — you can still check manually from any app's details screen. - Every 3 hours - Every 6 hours - Every 12 hours - Daily + Every %1$d hours + Daily + Every %1$d days + Weekly + Every 2 weeks + Every 30 days Allow background updates Your device aggressively kills background tasks. Whitelist GitHub Store from battery optimization so scheduled update checks and silent installs can run reliably. diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index debd6968b..66692e354 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -829,19 +829,42 @@ private fun AutoUpdateCard( } } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +private val IntervalStops: List = listOf(3L, 6L, 12L, 24L, 72L, 168L, 336L, 720L) + +@Composable +private fun formatIntervalLabel(hours: Long): String { + val days = hours / 24 + return when { + hours < 24 -> stringResource(Res.string.interval_every_hours, hours.toInt()) + hours == 24L -> stringResource(Res.string.interval_daily) + hours == 168L -> stringResource(Res.string.interval_weekly) + hours == 336L -> stringResource(Res.string.interval_biweekly) + hours == 720L -> stringResource(Res.string.interval_monthly) + else -> stringResource(Res.string.interval_every_days, days.toInt()) + } +} + +@Composable +private fun formatIntervalShort(hours: Long): String { + val days = hours / 24 + return when { + hours < 24 -> "${hours}h" + days < 30 -> "${days}d" + else -> "30d" + } +} + @Composable private fun UpdateCheckIntervalCard( selectedIntervalHours: Long, enabled: Boolean, onIntervalSelected: (Long) -> Unit, ) { - val intervals = listOf( - 3L to Res.string.interval_3h, - 6L to Res.string.interval_6h, - 12L to Res.string.interval_12h, - 24L to Res.string.interval_24h, - ) + val currentIndex = IntervalStops.indexOf(selectedIntervalHours) + .let { if (it == -1) IntervalStops.indexOf(IntervalStops.minByOrNull { stop -> kotlin.math.abs(stop - selectedIntervalHours) }) else it } + .coerceAtLeast(0) + val maxIndex = IntervalStops.lastIndex + val cs = MaterialTheme.colorScheme ExpressiveCard { Column( @@ -860,52 +883,63 @@ private fun UpdateCheckIntervalCard( modifier = Modifier .size(40.dp) .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) + .background(cs.primaryContainer) .padding(8.dp), - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = cs.onPrimaryContainer ) Column( + modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Text( text = stringResource(Res.string.update_check_interval_title), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, + color = cs.onSurface, fontWeight = FontWeight.SemiBold ) Text( text = stringResource(Res.string.update_check_interval_description), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = cs.onSurfaceVariant ) } } - FlowRow( + Text( + text = formatIntervalLabel(IntervalStops[currentIndex]), + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold, + ), + color = if (enabled) cs.primary else cs.onSurfaceVariant, + ) + + androidx.compose.material3.Slider( + value = currentIndex.toFloat(), + onValueChange = { v -> + val idx = v.toInt().coerceIn(0, maxIndex) + onIntervalSelected(IntervalStops[idx]) + }, + steps = maxIndex - 1, + valueRange = 0f..maxIndex.toFloat(), + enabled = enabled, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, ) { - intervals.forEach { (hours, labelRes) -> - val isSelected = selectedIntervalHours == hours - - FilterChip( - selected = isSelected, - enabled = enabled, - onClick = { onIntervalSelected(hours) }, - label = { - Text( - text = stringResource(labelRes), - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal - ) - }, - shape = RoundedCornerShape(12.dp), - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, - selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer, - ) - ) - } + Text( + text = formatIntervalShort(IntervalStops.first()), + style = MaterialTheme.typography.labelSmall, + color = cs.onSurfaceVariant, + ) + Text( + text = formatIntervalShort(IntervalStops.last()), + style = MaterialTheme.typography.labelSmall, + color = cs.onSurfaceVariant, + ) } } } From 77e8d4f803e24c549c17655641f83fe3e51d547a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 12:39:15 +0500 Subject: [PATCH 168/172] i18n(tweaks): translate interval strings to 12 locales --- .../composeResources/values-ar/strings-ar.xml | 11 ++++++----- .../composeResources/values-bn/strings-bn.xml | 11 ++++++----- .../composeResources/values-es/strings-es.xml | 11 ++++++----- .../composeResources/values-fr/strings-fr.xml | 11 ++++++----- .../composeResources/values-hi/strings-hi.xml | 11 ++++++----- .../composeResources/values-it/strings-it.xml | 11 ++++++----- .../composeResources/values-ja/strings-ja.xml | 11 ++++++----- .../composeResources/values-ko/strings-ko.xml | 11 ++++++----- .../composeResources/values-pl/strings-pl.xml | 11 ++++++----- .../composeResources/values-ru/strings-ru.xml | 11 ++++++----- .../composeResources/values-tr/strings-tr.xml | 11 ++++++----- .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 11 ++++++----- 12 files changed, 72 insertions(+), 60 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 1f2fef9ba..de1e7dd3d 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -452,11 +452,6 @@ عدد مرات التحقق من تحديثات التطبيق في الخلفية التحقق التلقائي من التحديثات يبحث عن التحديثات في الخلفية بشكل دوري. أوقفه لتوفير البطارية — يمكنك دائماً التحقق يدوياً من شاشة تفاصيل أي تطبيق. - ٣ ساعات - ٦ ساعات - ١٢ ساعة - ٢٤ ساعة - السماح بالتحديثات في الخلفية جهازك يوقف المهام الخلفية بقوة. أضف GitHub Store إلى قائمة الاستثناء من تحسين البطارية حتى تعمل عمليات فحص التحديثات والتثبيت الصامت بشكل موثوق. فتح الإعدادات @@ -1315,4 +1310,10 @@ الإصدار، المجتمع، القانوني قابل للتثبيت هنا لا توجد مستودعات بأصول قابلة للتثبيت لهذه المنصة + كل %1$d ساعة + يوميًا + كل %1$d أيام + أسبوعيًا + كل أسبوعين + كل 30 يومًا diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index fa5b8c1ec..23a51086e 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -452,11 +452,6 @@ ব্যাকগ্রাউন্ডে কতক্ষণ পর পর অ্যাপ আপডেট খোঁজা হবে ব্যাকগ্রাউন্ড আপডেট চেক নিয়মিত ব্যাকগ্রাউন্ডে আপডেট খোঁজে। ব্যাটারি বাঁচাতে বন্ধ করুন — আপনি যেকোনো অ্যাপের ডিটেইলস স্ক্রিন থেকে ম্যানুয়ালি চেক করতে পারবেন। - ৩ঘ - ৬ঘ - ১২ঘ - ২৪ঘ - ব্যাকগ্রাউন্ড আপডেট অনুমতি দিন আপনার ডিভাইস ব্যাকগ্রাউন্ড টাস্কগুলো আক্রমণাত্মকভাবে বন্ধ করে। GitHub Store-কে ব্যাটারি অপটিমাইজেশন থেকে হোয়াইটলিস্ট করুন যাতে নির্ধারিত আপডেট চেক এবং সাইলেন্ট ইনস্টল নির্ভরযোগ্যভাবে চলে। সেটিংস খুলুন @@ -1277,4 +1272,10 @@ সংস্করণ, কমিউনিটি, আইনি এখানে ইনস্টলযোগ্য এই প্ল্যাটফর্মের জন্য ইনস্টলযোগ্য অ্যাসেট সহ কোনো রিপো নেই + প্রতি %1$d ঘণ্টায় + প্রতিদিন + প্রতি %1$d দিনে + সাপ্তাহিক + প্রতি 2 সপ্তাহে + প্রতি 30 দিনে diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index c8d739408..57df04d24 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -419,11 +419,6 @@ Con qué frecuencia buscar actualizaciones en segundo plano Comprobación de actualizaciones en segundo plano Busca actualizaciones periódicamente en segundo plano. Desactívalo para ahorrar batería — puedes comprobar manualmente desde la pantalla de detalles de cualquier app. - 3h - 6h - 12h - 24h - Permitir actualizaciones en segundo plano Tu dispositivo cierra agresivamente las tareas en segundo plano. Excluye GitHub Store de la optimización de batería para que las comprobaciones programadas y las instalaciones silenciosas funcionen de forma fiable. Abrir ajustes @@ -1256,4 +1251,10 @@ Versión, comunidad, legal Instalable aquí Sin repositorios con assets instalables para esta plataforma + Cada %1$d horas + Diariamente + Cada %1$d días + Semanalmente + Cada 2 semanas + Cada 30 días diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index 736ed43f2..58b19cd00 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -420,11 +420,6 @@ Fréquence de vérification des mises à jour en arrière-plan Vérification des mises à jour en arrière-plan Vérifie périodiquement les mises à jour en arrière-plan. Désactivez pour économiser la batterie — vous pouvez vérifier manuellement depuis la fiche détails de n'importe quelle app. - 3h - 6h - 12h - 24h - Autoriser les mises à jour en arrière-plan Votre appareil interrompt agressivement les tâches d'arrière-plan. Excluez GitHub Store de l'optimisation de la batterie pour que les vérifications planifiées et les installations silencieuses fonctionnent de manière fiable. Ouvrir les paramètres @@ -1256,4 +1251,10 @@ Version, communauté, mentions légales Installable ici Aucun dépôt avec des assets installables pour cette plateforme + Toutes les %1$d heures + Quotidien + Tous les %1$d jours + Hebdomadaire + Toutes les 2 semaines + Tous les 30 jours diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index edbdee6f9..c93332df1 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -450,11 +450,6 @@ पृष्ठभूमि में ऐप अपडेट कितनी बार जाँचें पृष्ठभूमि अपडेट जाँच पृष्ठभूमि में समय-समय पर अपडेट जाँचता है। बैटरी बचाने के लिए बंद करें — किसी भी ऐप के विवरण स्क्रीन से मैन्युअली जाँच की जा सकती है। - 3घ - 6घ - 12घ - 24घ - बैकग्राउंड अपडेट की अनुमति दें आपका डिवाइस बैकग्राउंड कार्य आक्रामक रूप से बंद कर देता है। GitHub Store को बैटरी ऑप्टिमाइज़ेशन से व्हाइटलिस्ट करें ताकि निर्धारित अपडेट चेक और साइलेंट इंस्टॉल विश्वसनीय रूप से चलें। सेटिंग्स खोलें @@ -1292,4 +1287,10 @@ संस्करण, समुदाय, कानूनी यहाँ इंस्टॉल योग्य इस प्लेटफ़ॉर्म के लिए इंस्टॉल योग्य ऐसेट वाले रिपॉज़िटरी नहीं + हर %1$d घंटे + रोज़ाना + हर %1$d दिन + साप्ताहिक + हर 2 सप्ताह + हर 30 दिन diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 8ccb38a10..c3117adb5 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -452,11 +452,6 @@ Ogni quanto verificare gli aggiornamenti in background Verifica aggiornamenti in background Verifica periodicamente gli aggiornamenti in background. Disattiva per risparmiare batteria — puoi sempre verificare manualmente dalla schermata dettagli di qualsiasi app. - 3h - 6h - 12h - 24h - Consenti aggiornamenti in background Il tuo dispositivo chiude in modo aggressivo le attività in background. Esenta GitHub Store dall'ottimizzazione della batteria affinché i controlli pianificati e le installazioni silenziose funzionino in modo affidabile. Apri impostazioni @@ -1290,4 +1285,10 @@ Versione, community, legale Installabile qui Nessun repository con asset installabili per questa piattaforma + Ogni %1$d ore + Quotidiano + Ogni %1$d giorni + Settimanale + Ogni 2 settimane + Ogni 30 giorni diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 851d2558c..dbee65ba4 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -421,11 +421,6 @@ バックグラウンドでアプリのアップデートを確認する頻度 バックグラウンド更新確認 バックグラウンドで定期的に更新を確認します。バッテリー節約のためオフにできます — 各アプリの詳細画面から手動で確認できます。 - 3時間 - 6時間 - 12時間 - 24時間 - バックグラウンド更新を許可 このデバイスはバックグラウンドタスクを積極的に終了します。バッテリー最適化からGitHub Storeを除外して、定期的な更新チェックとサイレントインストールが確実に動作するようにしましょう。 設定を開く @@ -1246,4 +1241,10 @@ バージョン、コミュニティ、法的情報 ここでインストール可能 このプラットフォーム向けのインストール可能なアセットを持つリポジトリはありません + %1$d 時間ごと + 毎日 + %1$d 日ごと + 毎週 + 2 週間ごと + 30 日ごと diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 8d79f2208..eb0b7607b 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -450,11 +450,6 @@ 백그라운드에서 앱 업데이트를 확인하는 빈도 백그라운드 업데이트 확인 백그라운드에서 주기적으로 업데이트를 확인합니다. 배터리를 절약하려면 끄세요 — 앱 세부 정보 화면에서 언제든 수동으로 확인할 수 있습니다. - 3시간 - 6시간 - 12시간 - 24시간 - 백그라운드 업데이트 허용 이 기기는 백그라운드 작업을 적극적으로 종료해요. 배터리 최적화에서 GitHub Store를 화이트리스트에 추가하면 예약된 업데이트 확인과 사일런트 설치가 안정적으로 동작해요. 설정 열기 @@ -1276,4 +1271,10 @@ 버전, 커뮤니티, 법적 고지 여기서 설치 가능 이 플랫폼용 설치 가능한 자산이 있는 저장소가 없습니다 + %1$d 시간마다 + 매일 + %1$d 일마다 + 매주 + 2주마다 + 30일마다 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index d931d74cc..a4bc7e853 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -422,11 +422,6 @@ Jak często sprawdzać aktualizacje aplikacji w tle Sprawdzanie aktualizacji w tle Okresowo sprawdza aktualizacje w tle. Wyłącz, aby oszczędzać baterię — zawsze możesz sprawdzić ręcznie z ekranu szczegółów dowolnej aplikacji. - 3g - 6g - 12g - 24g - Zezwól na aktualizacje w tle Twoje urządzenie agresywnie zamyka zadania w tle. Wyłącz optymalizację baterii dla GitHub Store, aby zaplanowane sprawdzenia aktualizacji i ciche instalacje działały niezawodnie. Otwórz ustawienia @@ -1285,4 +1280,10 @@ Wersja, społeczność, informacje prawne Instalowalne tutaj Brak repozytoriów z plikami instalowalnymi dla tej platformy + Co %1$d godziny + Codziennie + Co %1$d dni + Co tydzień + Co 2 tygodnie + Co 30 dni diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 8419bda60..3bd81a022 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -422,11 +422,6 @@ Как часто проверять обновления приложения в фоне Фоновая проверка обновлений Периодически проверяет обновления в фоне. Отключи для экономии батареи — всегда можно проверить вручную из экрана деталей любого приложения. - - - 12ч - 24ч - Разрешить обновления в фоне Твоё устройство агрессивно завершает фоновые задачи. Исключи GitHub Store из оптимизации батареи, чтобы запланированные проверки обновлений и тихая установка работали надёжно. Открыть настройки @@ -1285,4 +1280,10 @@ Версия, сообщество, юридическая информация Можно установить здесь Нет репозиториев с устанавливаемыми сборками для этой платформы + Каждые %1$d часов + Ежедневно + Каждые %1$d дней + Еженедельно + Раз в 2 недели + Каждые 30 дней diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 5d151ece6..6a8073e41 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -450,11 +450,6 @@ Arka planda uygulama güncellemelerinin ne sıklıkla kontrol edileceği Arka planda güncelleme kontrolü Arka planda düzenli aralıklarla güncellemeleri kontrol eder. Pil tasarrufu için kapatın — her zaman uygulamanın detay ekranından manuel olarak kontrol edebilirsiniz. - 3s - 6s - 12s - 24s - Arka planda güncellemelere izin ver Cihazın arka plan görevlerini agresif şekilde kapatıyor. GitHub Store'u pil optimizasyonundan hariç tut ki zamanlanmış güncelleme kontrolleri ve sessiz kurulumlar güvenilir biçimde çalışsın. Ayarları aç @@ -1287,4 +1282,10 @@ Sürüm, topluluk, yasal Burada kurulabilir Bu platform için kurulabilir varlığı olan depo yok + %1$d saatte bir + Günlük + %1$d günde bir + Haftalık + 2 haftada bir + 30 günde bir diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index cca4a2fc0..005d5a2b9 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -422,11 +422,6 @@ 在后台检查应用更新的频率 后台检查更新 在后台周期性检查更新。可关闭以节省电量 — 你随时可在任意应用的详情页手动检查。 - 3小时 - 6小时 - 12小时 - 24小时 - 允许后台更新 你的设备会积极结束后台任务。把 GitHub Store 加入电池优化白名单,定时更新检查和静默安装才能稳定运行。 打开设置 @@ -1249,4 +1244,10 @@ 版本、社区、法律 可在此安装 没有适用于当前平台的可安装资源仓库 + 每 %1$d 小时 + 每天 + 每 %1$d 天 + 每周 + 每 2 周 + 每 30 天 From 7604ddcd8b8f93100e965835a2413fe65d1c2886 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 14:05:30 +0500 Subject: [PATCH 169/172] feat(anim): nav slide transitions + lazy list animateItem on home --- .../app/navigation/AppNavigation.kt | 40 +++++++++++++++++++ .../zed/rainxch/home/presentation/HomeRoot.kt | 4 ++ 2 files changed, 44 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index b9b9de899..17966cb3b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -153,6 +153,46 @@ fun AppNavigation( navController = navController, startDestination = GithubStoreGraph.HomeScreen, modifier = Modifier.background(MaterialTheme.colorScheme.background), + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { it / 6 }, + animationSpec = + androidx.compose.animation.core + .tween(280), + ) + + androidx.compose.animation.fadeIn( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + }, + exitTransition = { + androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(180), + ) + }, + popEnterTransition = { + androidx.compose.animation.fadeIn( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + }, + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { it / 6 }, + animationSpec = + androidx.compose.animation.core + .tween(280), + ) + + androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + }, ) { composable { val listDetailState = rememberAdaptiveListDetailState() diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index ba546f0b0..2f77b745a 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -215,6 +215,7 @@ private fun HomeScreen( card = card, onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + modifier = Modifier.animateItem(), ) } item(key = "hot_see_all") { @@ -243,6 +244,7 @@ private fun HomeScreen( rank = index + 1, onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + modifier = Modifier.animateItem(), ) } item(key = "trending_see_more") { @@ -267,6 +269,7 @@ private fun HomeScreen( rank = index + 1, onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + modifier = Modifier.animateItem(), ) } item(key = "popular_see_more") { @@ -287,6 +290,7 @@ private fun HomeScreen( card = card, onClick = { onAction(HomeAction.OnRepoClick(card.rawRepository)) }, onLongClick = { onAction(HomeAction.OnRepoLongClick(card.id)) }, + modifier = Modifier.animateItem(), ) } item(key = "starred_see_more") { From 7d5fdaaec01995ce873ef6699b8e2391f93bf8e8 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 14:09:57 +0500 Subject: [PATCH 170/172] feat(anim): shared element container transform repo card to details --- .../app/navigation/AppNavigation.kt | 1479 +++++++++-------- .../presentation/components/RepositoryCard.kt | 14 + .../locals/SharedTransitionLocals.kt | 11 + .../details/presentation/DetailsRoot.kt | 16 + 4 files changed, 788 insertions(+), 732 deletions(-) create mode 100644 core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/SharedTransitionLocals.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 17966cb3b..b524ed932 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -149,184 +149,231 @@ fun AppNavigation( Modifier.fillMaxSize() }, ) { - NavHost( - navController = navController, - startDestination = GithubStoreGraph.HomeScreen, - modifier = Modifier.background(MaterialTheme.colorScheme.background), - enterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { it / 6 }, - animationSpec = - androidx.compose.animation.core - .tween(280), - ) + - androidx.compose.animation.fadeIn( + androidx.compose.animation.SharedTransitionLayout { + val sharedScope = this + NavHost( + navController = navController, + startDestination = GithubStoreGraph.HomeScreen, + modifier = Modifier.background(MaterialTheme.colorScheme.background), + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { it / 6 }, animationSpec = androidx.compose.animation.core - .tween(220), - ) - }, - exitTransition = { - androidx.compose.animation.fadeOut( - animationSpec = - androidx.compose.animation.core - .tween(180), - ) - }, - popEnterTransition = { - androidx.compose.animation.fadeIn( - animationSpec = - androidx.compose.animation.core - .tween(220), - ) - }, - popExitTransition = { - androidx.compose.animation.slideOutHorizontally( - targetOffsetX = { it / 6 }, - animationSpec = - androidx.compose.animation.core - .tween(280), - ) + + .tween(280), + ) + + androidx.compose.animation.fadeIn( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + }, + exitTransition = { androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(180), + ) + }, + popEnterTransition = { + androidx.compose.animation.fadeIn( animationSpec = androidx.compose.animation.core .tween(220), ) - }, - ) { - composable { - val listDetailState = rememberAdaptiveListDetailState() - val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) - val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) - AdaptiveListDetailScaffold( - state = listDetailState, - emptyPaneTitle = pickRepoTitle, - emptyPaneSubtitle = pickRepoSubtitle, - list = { isExpanded -> - HomeRoot( - onNavigateToSearch = { - navController.navigate(GithubStoreGraph.SearchScreen()) - }, - onNavigateToSettings = { - navController.navigate(GithubStoreGraph.ProfileScreen) - }, - onNavigateToApps = { - navController.navigate(GithubStoreGraph.AppsScreen) - }, - onNavigateToDetails = { repoId -> - if (isExpanded) { - listDetailState.select( - AdaptiveDetailArgs(repositoryId = repoId), - ) - } else { - navController.navigate( - GithubStoreGraph.DetailsScreen(repositoryId = repoId), - ) - } - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen(username = username), + }, + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { it / 6 }, + animationSpec = + androidx.compose.animation.core + .tween(280), + ) + + androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + }, + ) { + composable { + val animatedScope = this + CompositionLocalProvider( + zed.rainxch.core.presentation.locals.LocalSharedTransitionScope provides sharedScope, + zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope provides animatedScope, + ) { + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + HomeRoot( + onNavigateToSearch = { + navController.navigate(GithubStoreGraph.SearchScreen()) + }, + onNavigateToSettings = { + navController.navigate(GithubStoreGraph.ProfileScreen) + }, + onNavigateToApps = { + navController.navigate(GithubStoreGraph.AppsScreen) + }, + onNavigateToDetails = { repoId -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs(repositoryId = repoId), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen(repositoryId = repoId), + ) + } + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, + onNavigateToCategoryList = { category -> + navController.navigate( + GithubStoreGraph.CategoryListScreen(category.name), + ) + }, + onNavigateToStarredRepos = { + navController.navigate(GithubStoreGraph.StarredReposScreen) + }, ) }, - onNavigateToCategoryList = { category -> - navController.navigate( - GithubStoreGraph.CategoryListScreen(category.name), + detail = { args -> + AdaptiveDetailPaneContent( + args = args, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, ) }, - onNavigateToStarredRepos = { - navController.navigate(GithubStoreGraph.StarredReposScreen) - }, - ) - }, - detail = { args -> - AdaptiveDetailPaneContent( - args = args, - navController = navController, - onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, - onClearPane = { listDetailState.clear() }, ) - }, - ) - } + } + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - val category = - runCatching { - zed.rainxch.home.domain.model.HomeCategory - .valueOf(args.category) - }.getOrDefault( - zed.rainxch.home.domain.model.HomeCategory.HOT_RELEASE, - ) - zed.rainxch.home.presentation.categorylist.CategoryListRoot( - category = category, - onNavigateBack = { navController.navigateUp() }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen(repositoryId = repoId), + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val category = + runCatching { + zed.rainxch.home.domain.model.HomeCategory + .valueOf(args.category) + }.getOrDefault( + zed.rainxch.home.domain.model.HomeCategory.HOT_RELEASE, ) - }, - ) - } + zed.rainxch.home.presentation.categorylist.CategoryListRoot( + category = category, + onNavigateBack = { navController.navigateUp() }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen(repositoryId = repoId), + ) + }, + ) + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - val initialPlatform = - args.initialPlatform?.let { name -> - runCatching { - SearchPlatformUi.valueOf(name) - }.getOrNull() - } - val listDetailState = rememberAdaptiveListDetailState() - val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) - val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) - val searchViewModel: zed.rainxch.search.presentation.SearchViewModel = - koinViewModel { - parametersOf(initialPlatform) - } - AdaptiveListDetailScaffold( - state = listDetailState, - emptyPaneTitle = pickRepoTitle, - emptyPaneSubtitle = pickRepoSubtitle, - list = { isExpanded -> - SearchRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId, sourceHost -> - if (isExpanded) { - listDetailState.select( - AdaptiveDetailArgs( - repositoryId = repoId, - sourceHost = sourceHost, - ), - ) - } else { + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val initialPlatform = + args.initialPlatform?.let { name -> + runCatching { + SearchPlatformUi.valueOf(name) + }.getOrNull() + } + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + val searchViewModel: zed.rainxch.search.presentation.SearchViewModel = + koinViewModel { + parametersOf(initialPlatform) + } + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + SearchRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId, sourceHost -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + repositoryId = repoId, + sourceHost = sourceHost, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + sourceHost = sourceHost, + ), + ) + } + }, + onNavigateToDetailsFromLink = { owner, repo -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + owner = owner, + repo = repo, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + owner = owner, + repo = repo, + ), + ) + } + }, + onNavigateToDeveloperProfile = { username -> navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - sourceHost = sourceHost, + GithubStoreGraph.DeveloperProfileScreen( + username = username, ), ) - } + }, + viewModel = searchViewModel, + ) + }, + detail = { detailArgs -> + AdaptiveDetailPaneContent( + args = detailArgs, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, + ) + }, + ) + } + + composable { backStackEntry -> + val animatedScope = this + CompositionLocalProvider( + zed.rainxch.core.presentation.locals.LocalSharedTransitionScope provides sharedScope, + zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope provides animatedScope, + ) { + val args = backStackEntry.toRoute() + DetailsRoot( + onNavigateBack = { + navController.navigateUp() }, - onNavigateToDetailsFromLink = { owner, repo -> - if (isExpanded) { - listDetailState.select( - AdaptiveDetailArgs( - owner = owner, - repo = repo, - ), - ) - } else { - navController.navigate( - GithubStoreGraph.DetailsScreen( - owner = owner, - repo = repo, - ), - ) - } + onOpenRepositoryInApp = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) }, onNavigateToDeveloperProfile = { username -> navController.navigate( @@ -335,629 +382,597 @@ fun AppNavigation( ), ) }, - viewModel = searchViewModel, + onNavigateToSearchByPlatform = { platform -> + navController.navigate( + GithubStoreGraph.SearchScreen( + initialPlatform = platform.toSearchPlatformUi().name, + ), + ) + }, + onNavigateToAbout = { repoId, owner, repo, sourceHost -> + navController.navigate( + GithubStoreGraph.DetailsAboutScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + sourceHost = sourceHost, + ), + ) + }, + onNavigateToWhatsNew = { repoId, owner, repo, sourceHost -> + navController.navigate( + GithubStoreGraph.DetailsWhatsNewScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + sourceHost = sourceHost, + ), + ) + }, + viewModel = + koinViewModel { + parametersOf( + args.repositoryId, + args.owner, + args.repo, + args.isComingFromUpdate, + args.sourceHost, + ) + }, + ) + } + } + + composable( + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - detail = { detailArgs -> - AdaptiveDetailPaneContent( - args = detailArgs, - navController = navController, - onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, - onClearPane = { listDetailState.clear() }, + exitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - ) - } - - composable { backStackEntry -> - val args = backStackEntry.toRoute() - DetailsRoot( - onNavigateBack = { - navController.navigateUp() + popEnterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), + ) }, - onOpenRepositoryInApp = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), + ) { backStackEntry -> + val args = backStackEntry.toRoute() + AboutRoot( + repositoryId = args.repositoryId, + owner = args.owner, + repo = args.repo, + sourceHost = args.sourceHost, + onNavigateBack = { navController.navigateUp() }, + ) + } + + composable( + enterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - onNavigateToSearchByPlatform = { platform -> - navController.navigate( - GithubStoreGraph.SearchScreen( - initialPlatform = platform.toSearchPlatformUi().name, - ), + exitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - onNavigateToAbout = { repoId, owner, repo, sourceHost -> - navController.navigate( - GithubStoreGraph.DetailsAboutScreen( - repositoryId = repoId, - owner = owner, - repo = repo, - sourceHost = sourceHost, - ), + popEnterTransition = { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth / 4 }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - onNavigateToWhatsNew = { repoId, owner, repo, sourceHost -> - navController.navigate( - GithubStoreGraph.DetailsWhatsNewScreen( - repositoryId = repoId, - owner = owner, - repo = repo, - sourceHost = sourceHost, - ), + popExitTransition = { + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = + androidx.compose.animation.core + .tween(durationMillis = 280), ) }, - viewModel = - koinViewModel { - parametersOf( - args.repositoryId, - args.owner, - args.repo, - args.isComingFromUpdate, - args.sourceHost, + ) { backStackEntry -> + val args = backStackEntry.toRoute() + WhatsNewRoot( + repositoryId = args.repositoryId, + owner = args.owner, + repo = args.repo, + sourceHost = args.sourceHost, + onNavigateBack = { navController.navigateUp() }, + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + DeveloperProfileRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), ) }, - ) - } - - composable( - enterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - exitTransition = { - androidx.compose.animation.slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 4 }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - popEnterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 4 }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - popExitTransition = { - androidx.compose.animation.slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), + onNavigateToUser = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen(username = username), + ) + }, + viewModel = + koinViewModel { + parametersOf(args.username) + }, ) - }, - ) { backStackEntry -> - val args = backStackEntry.toRoute() - AboutRoot( - repositoryId = args.repositoryId, - owner = args.owner, - repo = args.repo, - sourceHost = args.sourceHost, - onNavigateBack = { navController.navigateUp() }, - ) - } + } - composable( - enterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - exitTransition = { - androidx.compose.animation.slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 4 }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - popEnterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 4 }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), - ) - }, - popExitTransition = { - androidx.compose.animation.slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = - androidx.compose.animation.core - .tween(durationMillis = 280), + composable { + AuthenticationRoot( + onNavigateToHome = { + navController.navigate(GithubStoreGraph.HomeScreen) { + popUpTo(0) { + inclusive = true + } + } + }, ) - }, - ) { backStackEntry -> - val args = backStackEntry.toRoute() - WhatsNewRoot( - repositoryId = args.repositoryId, - owner = args.owner, - repo = args.repo, - sourceHost = args.sourceHost, - onNavigateBack = { navController.navigateUp() }, - ) - } + } - composable { backStackEntry -> - val args = backStackEntry.toRoute() - DeveloperProfileRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToUser = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen(username = username), - ) - }, - viewModel = - koinViewModel { - parametersOf(args.username) + composable { + zed.rainxch.githubstore.app.onboarding.OnboardingRoot( + onNavigateToSignIn = { + navController.navigate(GithubStoreGraph.AuthenticationScreen) }, - ) - } - - composable { - AuthenticationRoot( - onNavigateToHome = { - navController.navigate(GithubStoreGraph.HomeScreen) { - popUpTo(0) { - inclusive = true + onNavigateToHome = { + navController.navigate(GithubStoreGraph.HomeScreen) { + popUpTo(0) { inclusive = true } } - } - }, - ) - } - - composable { - zed.rainxch.githubstore.app.onboarding.OnboardingRoot( - onNavigateToSignIn = { - navController.navigate(GithubStoreGraph.AuthenticationScreen) - }, - onNavigateToHome = { - navController.navigate(GithubStoreGraph.HomeScreen) { - popUpTo(0) { inclusive = true } - } - }, - ) - } + }, + ) + } - composable { - FavouritesRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { - navController.navigate(GithubStoreGraph.DetailsScreen(it)) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + FavouritesRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { + navController.navigate(GithubStoreGraph.DetailsScreen(it)) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - StarredReposRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToAuthentication = { - navController.navigate( - GithubStoreGraph.AuthenticationScreen, - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + StarredReposRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + onNavigateToAuthentication = { + navController.navigate( + GithubStoreGraph.AuthenticationScreen, + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - zed.rainxch.apps.presentation.starred.StarredPickerRoot( - onNavigateBack = { navController.navigateUp() }, - onNavigateToDetails = { repoId, owner, repo -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - owner = owner, - repo = repo, - ), - ) - }, - ) - } + composable { + zed.rainxch.apps.presentation.starred.StarredPickerRoot( + onNavigateBack = { navController.navigateUp() }, + onNavigateToDetails = { repoId, owner, repo -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + owner = owner, + repo = repo, + ), + ) + }, + ) + } - composable { - ProfileRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToAuthentication = { - navController.navigate(GithubStoreGraph.AuthenticationScreen) - }, - onNavigateToStarredRepos = { - navController.navigate(GithubStoreGraph.StarredReposScreen) - }, - onNavigateToFavouriteRepos = { - navController.navigate(GithubStoreGraph.FavouritesScreen) - }, - onNavigateToRecentlyViewed = { - navController.navigate(GithubStoreGraph.RecentlyViewedScreen) - }, - onNavigateToDevProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username, - ), - ) - }, - onNavigateToWhatsNew = { - navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) - }, - onPreviewWhatsNewSheet = { - whatsNewViewModel.forceShowLatest() - navController.navigateUp() - }, - onNavigateToAnnouncements = { - navController.navigate(GithubStoreGraph.AnnouncementsScreen) - }, - onPreviewAnnouncements = { - announcementsViewModel.previewSampleAnnouncements() - navController.navigate(GithubStoreGraph.AnnouncementsScreen) - }, - onNavigateToTweaks = { - navController.navigate(GithubStoreGraph.TweaksScreen) - }, - onNavigateToAbout = { - navController.navigate(GithubStoreGraph.AboutScreen) - }, - hasUnreadAnnouncements = announcementsUnreadCount > 0, - ) - } + composable { + ProfileRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToAuthentication = { + navController.navigate(GithubStoreGraph.AuthenticationScreen) + }, + onNavigateToStarredRepos = { + navController.navigate(GithubStoreGraph.StarredReposScreen) + }, + onNavigateToFavouriteRepos = { + navController.navigate(GithubStoreGraph.FavouritesScreen) + }, + onNavigateToRecentlyViewed = { + navController.navigate(GithubStoreGraph.RecentlyViewedScreen) + }, + onNavigateToDevProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username, + ), + ) + }, + onNavigateToWhatsNew = { + navController.navigate(GithubStoreGraph.WhatsNewHistoryScreen) + }, + onPreviewWhatsNewSheet = { + whatsNewViewModel.forceShowLatest() + navController.navigateUp() + }, + onNavigateToAnnouncements = { + navController.navigate(GithubStoreGraph.AnnouncementsScreen) + }, + onPreviewAnnouncements = { + announcementsViewModel.previewSampleAnnouncements() + navController.navigate(GithubStoreGraph.AnnouncementsScreen) + }, + onNavigateToTweaks = { + navController.navigate(GithubStoreGraph.TweaksScreen) + }, + onNavigateToAbout = { + navController.navigate(GithubStoreGraph.AboutScreen) + }, + hasUnreadAnnouncements = announcementsUnreadCount > 0, + ) + } - composable { - RecentlyViewedRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - ), - ) - }, - onNavigateToDeveloperProfile = { username -> - navController.navigate( - GithubStoreGraph.DeveloperProfileScreen( - username = username, - ), - ) - }, - ) - } + composable { + RecentlyViewedRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + ), + ) + }, + onNavigateToDeveloperProfile = { username -> + navController.navigate( + GithubStoreGraph.DeveloperProfileScreen( + username = username, + ), + ) + }, + ) + } - composable { - MirrorPickerRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + MirrorPickerRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - val historyEntries by whatsNewViewModel.historyEntries.collectAsStateWithLifecycle() - WhatsNewHistoryScreen( - entries = historyEntries, - onNavigateBack = { navController.navigateUp() }, - ) - } + composable { + val historyEntries by whatsNewViewModel.historyEntries.collectAsStateWithLifecycle() + WhatsNewHistoryScreen( + entries = historyEntries, + onNavigateBack = { navController.navigateUp() }, + ) + } - composable { - val feed by announcementsViewModel.feed.collectAsStateWithLifecycle() - val displayed by announcementsViewModel.displayedItems.collectAsStateWithLifecycle() - AnnouncementsRoot( - items = displayed, - acknowledgedIds = feed.acknowledgedIds, - mutedCategories = feed.mutedCategories, - refreshFailed = feed.lastRefreshFailed, - onNavigateBack = { navController.navigateUp() }, - onRefresh = { announcementsViewModel.refresh() }, - onCtaClick = { announcementsViewModel.openCta(it) }, - onDismissClick = { announcementsViewModel.dismiss(it) }, - onAcknowledgeClick = { announcementsViewModel.acknowledge(it) }, - onToggleMute = { category, muted -> - announcementsViewModel.setMuted(category, muted) - }, - onLeavingScreen = { announcementsViewModel.clearPreview() }, - onEnteringScreen = { announcementsViewModel.markRoutineItemsSeen() }, - ) - } + composable { + val feed by announcementsViewModel.feed.collectAsStateWithLifecycle() + val displayed by announcementsViewModel.displayedItems.collectAsStateWithLifecycle() + AnnouncementsRoot( + items = displayed, + acknowledgedIds = feed.acknowledgedIds, + mutedCategories = feed.mutedCategories, + refreshFailed = feed.lastRefreshFailed, + onNavigateBack = { navController.navigateUp() }, + onRefresh = { announcementsViewModel.refresh() }, + onCtaClick = { announcementsViewModel.openCta(it) }, + onDismissClick = { announcementsViewModel.dismiss(it) }, + onAcknowledgeClick = { announcementsViewModel.acknowledge(it) }, + onToggleMute = { category, muted -> + announcementsViewModel.setMuted(category, muted) + }, + onLeavingScreen = { announcementsViewModel.clearPreview() }, + onEnteringScreen = { announcementsViewModel.markRoutineItemsSeen() }, + ) + } - composable { - TweaksRoot( - onNavigateBack = { navController.popBackStack() }, - onNavigateToAppearance = { - navController.navigate(GithubStoreGraph.TweaksAppearanceScreen) { - launchSingleTop = true - } - }, - onNavigateToLanguage = { - navController.navigate(GithubStoreGraph.TweaksLanguageScreen) { - launchSingleTop = true - } - }, - onNavigateToConnection = { - navController.navigate(GithubStoreGraph.TweaksConnectionScreen) { - launchSingleTop = true - } - }, - onNavigateToSources = { - navController.navigate(GithubStoreGraph.TweaksSourcesScreen) { - launchSingleTop = true - } - }, - onNavigateToTranslation = { - navController.navigate(GithubStoreGraph.TweaksTranslationScreen) { - launchSingleTop = true - } - }, - onNavigateToInstallMethod = { - navController.navigate(GithubStoreGraph.TweaksInstallScreen) { - launchSingleTop = true - } - }, - onNavigateToUpdates = { - navController.navigate(GithubStoreGraph.TweaksUpdatesScreen) { - launchSingleTop = true - } - }, - onNavigateToStorage = { - navController.navigate(GithubStoreGraph.TweaksStorageScreen) { - launchSingleTop = true - } - }, - onNavigateToPrivacy = { - navController.navigate(GithubStoreGraph.TweaksPrivacyScreen) { - launchSingleTop = true - } - }, - onNavigateToHostTokens = { - navController.navigate(GithubStoreGraph.HostTokensScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToAppearance = { + navController.navigate(GithubStoreGraph.TweaksAppearanceScreen) { + launchSingleTop = true + } + }, + onNavigateToLanguage = { + navController.navigate(GithubStoreGraph.TweaksLanguageScreen) { + launchSingleTop = true + } + }, + onNavigateToConnection = { + navController.navigate(GithubStoreGraph.TweaksConnectionScreen) { + launchSingleTop = true + } + }, + onNavigateToSources = { + navController.navigate(GithubStoreGraph.TweaksSourcesScreen) { + launchSingleTop = true + } + }, + onNavigateToTranslation = { + navController.navigate(GithubStoreGraph.TweaksTranslationScreen) { + launchSingleTop = true + } + }, + onNavigateToInstallMethod = { + navController.navigate(GithubStoreGraph.TweaksInstallScreen) { + launchSingleTop = true + } + }, + onNavigateToUpdates = { + navController.navigate(GithubStoreGraph.TweaksUpdatesScreen) { + launchSingleTop = true + } + }, + onNavigateToStorage = { + navController.navigate(GithubStoreGraph.TweaksStorageScreen) { + launchSingleTop = true + } + }, + onNavigateToPrivacy = { + navController.navigate(GithubStoreGraph.TweaksPrivacyScreen) { + launchSingleTop = true + } + }, + onNavigateToHostTokens = { + navController.navigate(GithubStoreGraph.HostTokensScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - TweaksAppearanceRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksAppearanceRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksLanguageRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksLanguageRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksConnectionRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksConnectionRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksSourcesRoot( - onNavigateBack = { navController.popBackStack() }, - onNavigateToMirrorPicker = { - navController.navigate(GithubStoreGraph.MirrorPickerScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksSourcesRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToMirrorPicker = { + navController.navigate(GithubStoreGraph.MirrorPickerScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - TweaksTranslationRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksTranslationRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksInstallRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksInstallRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksUpdatesRoot( - onNavigateBack = { navController.popBackStack() }, - onNavigateToSkippedUpdates = { - navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksUpdatesRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToSkippedUpdates = { + navController.navigate(GithubStoreGraph.SkippedUpdatesScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - TweaksStorageRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + TweaksStorageRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - TweaksPrivacyRoot( - onNavigateBack = { navController.popBackStack() }, - onNavigateToHiddenRepositories = { - navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksPrivacyRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToHiddenRepositories = { + navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - TweaksAppInfoRoot( - onNavigateBack = { navController.popBackStack() }, - onNavigateToLicenses = { - navController.navigate(GithubStoreGraph.LicensesScreen) { - launchSingleTop = true - } - }, - ) - } + composable { + TweaksAppInfoRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToLicenses = { + navController.navigate(GithubStoreGraph.LicensesScreen) { + launchSingleTop = true + } + }, + ) + } - composable { - LicensesRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + LicensesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - SkippedUpdatesRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + SkippedUpdatesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - HiddenRepositoriesRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + HiddenRepositoriesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { - HostTokensRoot( - onNavigateBack = { navController.popBackStack() }, - ) - } + composable { + HostTokensRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } - composable { backStackEntry -> - LaunchedEffect(backStackEntry) { - val handle = backStackEntry.savedStateHandle - val openLinkSheet = - handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) - if (openLinkSheet == true) { - handle.remove(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) - appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) + composable { backStackEntry -> + LaunchedEffect(backStackEntry) { + val handle = backStackEntry.savedStateHandle + val openLinkSheet = + handle.get(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) + if (openLinkSheet == true) { + handle.remove(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY) + appsViewModel.onAction(zed.rainxch.apps.presentation.AppsAction.OnAddByLinkClick) + } } + val listDetailState = rememberAdaptiveListDetailState() + val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) + val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) + AdaptiveListDetailScaffold( + state = listDetailState, + emptyPaneTitle = pickRepoTitle, + emptyPaneSubtitle = pickRepoSubtitle, + list = { isExpanded -> + AppsRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToRepo = { repoId, sourceHost, owner, repo -> + if (isExpanded) { + listDetailState.select( + AdaptiveDetailArgs( + repositoryId = repoId, + isComingFromUpdate = true, + sourceHost = sourceHost, + owner = owner, + repo = repo, + ), + ) + } else { + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + isComingFromUpdate = true, + sourceHost = sourceHost, + owner = owner.orEmpty(), + repo = repo.orEmpty(), + ), + ) + } + }, + onNavigateToExternalImport = { + navController.navigate(GithubStoreGraph.ExternalImportScreen) + }, + onNavigateToStarredPicker = { + navController.navigate(GithubStoreGraph.StarredPickerScreen) + }, + viewModel = appsViewModel, + state = appsState, + ) + }, + detail = { detailArgs -> + AdaptiveDetailPaneContent( + args = detailArgs, + navController = navController, + onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, + onClearPane = { listDetailState.clear() }, + ) + }, + ) } - val listDetailState = rememberAdaptiveListDetailState() - val pickRepoTitle = stringResource(Res.string.adaptive_pick_repo_title) - val pickRepoSubtitle = stringResource(Res.string.adaptive_pick_repo_subtitle) - AdaptiveListDetailScaffold( - state = listDetailState, - emptyPaneTitle = pickRepoTitle, - emptyPaneSubtitle = pickRepoSubtitle, - list = { isExpanded -> - AppsRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToRepo = { repoId, sourceHost, owner, repo -> - if (isExpanded) { - listDetailState.select( - AdaptiveDetailArgs( - repositoryId = repoId, - isComingFromUpdate = true, - sourceHost = sourceHost, - owner = owner, - repo = repo, - ), - ) - } else { - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - isComingFromUpdate = true, - sourceHost = sourceHost, - owner = owner.orEmpty(), - repo = repo.orEmpty(), - ), - ) - } - }, - onNavigateToExternalImport = { - navController.navigate(GithubStoreGraph.ExternalImportScreen) - }, - onNavigateToStarredPicker = { - navController.navigate(GithubStoreGraph.StarredPickerScreen) - }, - viewModel = appsViewModel, - state = appsState, - ) - }, - detail = { detailArgs -> - AdaptiveDetailPaneContent( - args = detailArgs, - navController = navController, - onCrossNavToRepo = { newArgs -> listDetailState.select(newArgs) }, - onClearPane = { listDetailState.clear() }, - ) - }, - ) - } - composable { - ExternalImportRoot( - onNavigateBack = { - navController.navigateUp() - }, - onNavigateToDetails = { repoId -> - navController.navigate( - GithubStoreGraph.DetailsScreen( - repositoryId = repoId, - isComingFromUpdate = true, - ), - ) - }, - onAddManually = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY, true) - navController.navigateUp() - }, - ) + composable { + ExternalImportRoot( + onNavigateBack = { + navController.navigateUp() + }, + onNavigateToDetails = { repoId -> + navController.navigate( + GithubStoreGraph.DetailsScreen( + repositoryId = repoId, + isComingFromUpdate = true, + ), + ) + }, + onAddManually = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(EXTERNAL_IMPORT_OPEN_LINK_SHEET_KEY, true) + navController.navigateUp() + }, + ) + } } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 74b7ea1a9..0da37b8e7 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -111,8 +111,22 @@ fun RepositoryCard( val sheetEnabled = onHideClick != null val repo = discoveryRepositoryUi.repository + val shared = zed.rainxch.core.presentation.locals.LocalSharedTransitionScope.current + val animatedScope = zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope.current + val sharedModifier = if (shared != null && animatedScope != null) { + with(shared) { + Modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = "repo-${repo.id}"), + animatedVisibilityScope = animatedScope, + ) + } + } else { + Modifier + } + Surface( modifier = modifier + .then(sharedModifier) .fillMaxWidth() .alpha(contentAlpha), shape = zed.rainxch.core.presentation.theme.tokens.Radii.row, diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/SharedTransitionLocals.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/SharedTransitionLocals.kt new file mode 100644 index 000000000..ba57a856c --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/SharedTransitionLocals.kt @@ -0,0 +1,11 @@ +package zed.rainxch.core.presentation.locals + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.staticCompositionLocalOf + +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalSharedTransitionScope = staticCompositionLocalOf { null } + +val LocalAnimatedVisibilityScope = staticCompositionLocalOf { null } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index f5ce3aa7f..c1f1fc85e 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -500,6 +500,21 @@ fun DetailsScreen( onReadMoreAbout: (() -> Unit)? = null, onReadMoreWhatsNew: (() -> Unit)? = null, ) { + val shared = zed.rainxch.core.presentation.locals.LocalSharedTransitionScope.current + val animatedScope = zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope.current + val sharedModifier = state.repository?.let { repo -> + if (shared != null && animatedScope != null) { + with(shared) { + Modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = "repo-${repo.id}"), + animatedVisibilityScope = animatedScope, + ) + } + } else { + Modifier + } + } ?: Modifier + Box(modifier = sharedModifier) { Scaffold( topBar = { DetailsTopbar( @@ -689,6 +704,7 @@ fun DetailsScreen( } } } + } } @OptIn(ExperimentalMaterial3Api::class) From 66812717794c56ec18be2fd1613b6365eec55432 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 14:15:38 +0500 Subject: [PATCH 171/172] revert(anim): drop shared element wrap caused blank load delay --- .../presentation/components/RepositoryCard.kt | 14 -------------- .../rainxch/details/presentation/DetailsRoot.kt | 16 ---------------- 2 files changed, 30 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 0da37b8e7..74b7ea1a9 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -111,22 +111,8 @@ fun RepositoryCard( val sheetEnabled = onHideClick != null val repo = discoveryRepositoryUi.repository - val shared = zed.rainxch.core.presentation.locals.LocalSharedTransitionScope.current - val animatedScope = zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope.current - val sharedModifier = if (shared != null && animatedScope != null) { - with(shared) { - Modifier.sharedBounds( - sharedContentState = rememberSharedContentState(key = "repo-${repo.id}"), - animatedVisibilityScope = animatedScope, - ) - } - } else { - Modifier - } - Surface( modifier = modifier - .then(sharedModifier) .fillMaxWidth() .alpha(contentAlpha), shape = zed.rainxch.core.presentation.theme.tokens.Radii.row, diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index c1f1fc85e..f5ce3aa7f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -500,21 +500,6 @@ fun DetailsScreen( onReadMoreAbout: (() -> Unit)? = null, onReadMoreWhatsNew: (() -> Unit)? = null, ) { - val shared = zed.rainxch.core.presentation.locals.LocalSharedTransitionScope.current - val animatedScope = zed.rainxch.core.presentation.locals.LocalAnimatedVisibilityScope.current - val sharedModifier = state.repository?.let { repo -> - if (shared != null && animatedScope != null) { - with(shared) { - Modifier.sharedBounds( - sharedContentState = rememberSharedContentState(key = "repo-${repo.id}"), - animatedVisibilityScope = animatedScope, - ) - } - } else { - Modifier - } - } ?: Modifier - Box(modifier = sharedModifier) { Scaffold( topBar = { DetailsTopbar( @@ -704,7 +689,6 @@ fun DetailsScreen( } } } - } } @OptIn(ExperimentalMaterial3Api::class) From 70e803f11b6f09fcfe164d8c4d3efe950fb93711 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 26 May 2026 14:19:07 +0500 Subject: [PATCH 172/172] feat(anim): direction-aware bottom nav transitions --- .../app/navigation/AppNavigation.kt | 62 ++++++++++++++----- .../app/navigation/NavigationUtils.kt | 11 ++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index b524ed932..af3288b3a 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -156,24 +156,58 @@ fun AppNavigation( startDestination = GithubStoreGraph.HomeScreen, modifier = Modifier.background(MaterialTheme.colorScheme.background), enterTransition = { - androidx.compose.animation.slideInHorizontally( - initialOffsetX = { it / 6 }, - animationSpec = - androidx.compose.animation.core - .tween(280), - ) + - androidx.compose.animation.fadeIn( + val from = initialState.bottomNavIndex() + val to = targetState.bottomNavIndex() + if (from != null && to != null && from != to) { + val sign = if (to > from) 1 else -1 + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { it * sign }, animationSpec = androidx.compose.animation.core - .tween(220), - ) + .tween(280), + ) + + androidx.compose.animation.fadeIn( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + } else { + androidx.compose.animation.slideInHorizontally( + initialOffsetX = { it / 6 }, + animationSpec = + androidx.compose.animation.core + .tween(280), + ) + + androidx.compose.animation.fadeIn( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + } }, exitTransition = { - androidx.compose.animation.fadeOut( - animationSpec = - androidx.compose.animation.core - .tween(180), - ) + val from = initialState.bottomNavIndex() + val to = targetState.bottomNavIndex() + if (from != null && to != null && from != to) { + val sign = if (to > from) -1 else 1 + androidx.compose.animation.slideOutHorizontally( + targetOffsetX = { it * sign }, + animationSpec = + androidx.compose.animation.core + .tween(280), + ) + + androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(220), + ) + } else { + androidx.compose.animation.fadeOut( + animationSpec = + androidx.compose.animation.core + .tween(180), + ) + } }, popEnterTransition = { androidx.compose.animation.fadeIn( diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt index 0995fcc40..3f0783aef 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/NavigationUtils.kt @@ -3,6 +3,17 @@ package zed.rainxch.githubstore.app.navigation import androidx.navigation.NavBackStackEntry import androidx.navigation.toRoute +fun NavBackStackEntry?.bottomNavIndex(): Int? { + val route = this?.destination?.route ?: return null + return when { + route.contains("HomeScreen") -> 0 + route.contains("SearchScreen") -> 1 + route.contains("AppsScreen") -> 2 + route.contains("ProfileScreen") -> 3 + else -> null + } +} + fun NavBackStackEntry?.getCurrentScreen(): GithubStoreGraph? { if (this == null) return null val route = destination.route ?: return null

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 z

e(N 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 literal 0 HcmV?d00001 diff --git a/core/presentation/src/commonMain/composeResources/font/jetbrains_mono.ttf b/core/presentation/src/commonMain/composeResources/font/jetbrains_mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..aa310be8b717fe3774f9444dd89d5f4101cc6d10 GIT binary patch literal 187208 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�`t1>ioQcDTIG6x2X6$P-d@w| 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 literal 0 HcmV?d00001 diff --git a/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_bold.ttf b/core/presentation/src/commonMain/composeResources/font/jetbrains_mono_bold.ttf deleted file mode 100644 index 8c93043de6454ad2d5575f0751150c6551d9c588..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277828 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