diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 89b65713d..80bcb4040 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -27,6 +27,7 @@ jobs: # branch came from a fork, and the checked-out HEAD is an ephemeral merge commit that exists # nowhere and cannot be looked up by anyone who reads it off a device. Both head.* values are # null outside a pull request, where the defaults are already right. + VECTOR_BASE_REMOTE: upstream VECTOR_BUILD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }} VECTOR_BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" @@ -40,6 +41,12 @@ jobs: with: submodules: recursive fetch-depth: 0 + + - name: Fetch upstream JingMatrix Vector (only master + tags) + run: | + git remote add upstream https://github.com/JingMatrix/Vector.git + git fetch upstream master + git fetch upstream --tags --no-recurse-submodules # Before anything is built, because it needs nothing but Python and a translator's mistake # should not wait twenty minutes to surface. Crowdin owns the content of these files, so what @@ -57,25 +64,19 @@ jobs: # to it and cannot be given one, so a fork builds unsigned, as does any branch that is not # master: neither publishes anything. - name: Write key - if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }} + # pull_request事件不注入密钥,其余(workflow_dispatch / push master / tag)都注入 + if: ${{ github.event_name != 'pull_request' }} env: KEY_STORE: ${{ secrets.KEY_STORE }} KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} ALIAS: ${{ secrets.ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} - IS_UPSTREAM: ${{ github.repository_owner == 'JingMatrix' }} run: | set -euo pipefail if [ -z "$KEY_STORE" ]; then - if [ "$IS_UPSTREAM" = "true" ]; then - echo "::error::KEY_STORE is empty on ${{ github.ref }}. Refusing to publish an unsigned build." - exit 1 - fi - echo "No signing secret on this fork; building unsigned." + echo "::warning::No signing secret, building unsigned debug build." exit 0 fi - # Through the environment rather than interpolated into the script: a password holding a - # quote would otherwise be pasted into a shell word and mangled, or worse, executed. { echo "androidStorePassword=$KEY_STORE_PASSWORD" echo "androidKeyAlias=$ALIAS" @@ -169,112 +170,4 @@ jobs: path: | zygisk/build/symbols daemon/build/symbols - dex2oat/build/symbols - - # --- stable release -------------------------------------------------------------------- - # - # A pushed `v*` tag is a stable release, not a canary. It is published here from the same - # signed zips this build produced, so cutting a release is `git tag && git push` with nothing - # hand-uploaded afterwards — the hand-assembly is exactly how v2.0's assets came to disagree - # with zygisk/update.json (#811), and an automated attach cannot drift from what was built. - # - # Marked `--latest`, unlike the canaries below: this is what `releases/latest`, and therefore - # the manager's update check, resolves to. The body is the module's own zygisk/changelog.md - # under a GitHub H1 the release page shows. The changelog itself carries no title line — the - # manager renders it beneath its own heading, so a banner there would only read twice. - - name: Publish release - if: ${{ success() && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - # `%s` arguments, never the format string, so a `%` anywhere in the changelog is literal. - notes=$(printf '# 🎉 Vector %s 🎉\n\n%s\n' "${TAG#v}" "$(cat zygisk/changelog.md)") - - # Recreated rather than edited so a re-run replaces the assets instead of appending a - # second copy. No `--cleanup-tag`: the release is rebuilt, the pushed release tag is kept. - gh release delete "$TAG" --yes 2>/dev/null || true - gh release create "$TAG" \ - --latest \ - --title "Vector ${TAG}" \ - --notes "$notes" \ - zygisk/release/Vector-v*-Release.zip \ - zygisk/release/Vector-v*-Debug.zip - - # --- canary distribution --------------------------------------------------------------- - # - # The zips above are also attached to a prerelease, because an Actions artifact cannot be - # downloaded without a GitHub account — `GET /actions/artifacts//zip` answers 401 to an - # anonymous caller, while a release asset answers 206. Testing a canary is the lowest-friction - # way for an ordinary user to help, so it must not require handing an OAuth grant to anyone, - # and it must work for the many users who cannot reach GitHub's login page at all. - # - # Marked prerelease so `releases/latest` — which is what update checks read — keeps pointing - # at the last stable tag. - - name: Publish canary prerelease - if: >- - success() && - (github.event_name == 'workflow_dispatch' || - (github.event_name == 'push' && github.ref == 'refs/heads/master')) - env: - GH_TOKEN: ${{ github.token }} - VERSION_CODE: ${{ steps.prepareArtifact.outputs.versionCode }} - VERSION_NAME: ${{ steps.prepareArtifact.outputs.versionName }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} - run: | - set -euo pipefail - tag="canary-${VERSION_CODE}" - subject=$(git log -1 --pretty=%s) - short=$(git rev-parse --short HEAD) - - notes=$(cat </dev/null || true - gh release create "$tag" \ - --prerelease \ - --target "${{ github.sha }}" \ - --title "Vector ${VERSION_NAME} canary ${VERSION_CODE}" \ - --notes "$notes" \ - zygisk/release/Vector-v*-Release.zip \ - zygisk/release/Vector-v*-Debug.zip - - # Five is what a tester needs: enough to bisect a regression across a few days, few enough - # that the releases page is still mostly releases. Sorted by version code, which is the commit - # count and therefore monotonic, rather than by date, which reruns and reverts can disorder. - - name: Keep only the five most recent canaries - if: >- - success() && - (github.event_name == 'workflow_dispatch' || - (github.event_name == 'push' && github.ref == 'refs/heads/master')) - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - # `grep` exits 1 when nothing matches, which under `pipefail` would fail the step on a - # repository that has no canaries yet. Collected first, then acted on. - tags=$(gh release list --limit 100 --json tagName --jq '.[].tagName' | grep '^canary-' || true) - [ -n "$tags" ] || exit 0 - echo "$tags" | sort -t- -k2 -n -r | tail -n +6 | while read -r old; do - echo "Removing $old" - gh release delete "$old" --yes --cleanup-tag - done + dex2oat/build/symbols \ No newline at end of file diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml deleted file mode 100644 index ccaf1179c..000000000 --- a/.github/workflows/crowdin.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Crowdin Action - -on: - workflow_dispatch: - push: - branches: [ master ] - paths: - # Named rather than globbed, to match crowdin.yml. The trigger's matching rules and the - # CLI's are defined independently — notably on whether `*` may match nothing — and a - # trigger that quietly never fires is the worse failure. - - manager/src/main/res/values/strings.xml - - manager-ui/src/main/res/values/strings_appearance.xml - - manager-ui/src/main/res/values/strings_language.xml - - manager-ui/src/main/res/values/strings_logs.xml - - manager-ui/src/main/res/values/strings_modules.xml - - manager-ui/src/main/res/values/strings_nav.xml - - manager-ui/src/main/res/values/strings_store.xml - - manager-ui/src/main/res/values/strings_trace.xml - - daemon/src/main/res/values/strings.xml - -jobs: - synchronize-with-crowdin: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: crowdin action - uses: crowdin/github-action@v2 - with: - upload_translations: true - download_translations: false - upload_sources: true - config: 'crowdin.yml' - crowdin_branch_name: master - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} diff --git a/build.gradle.kts b/build.gradle.kts index f8c9c30f6..50301f53d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,17 +21,19 @@ plugins { } /** A ValueSource that executes 'git rev-list --count' to get the total commit count. */ -abstract class GitCommitCountValueSource : ValueSource { +abstract class GitCommitCountValueSource : ValueSource { + interface Parameters : ValueSourceParameters { + val baseRemoteName: Property + } @get:Inject abstract val execOperations: ExecOperations - override fun obtain(): String { + val remoteName = parameters.baseRemoteName.get() val output = ByteArrayOutputStream() val result = execOperations.exec { - commandLine("git", "rev-list", "--count", "refs/remotes/origin/master") + commandLine("git", "rev-list", "--count", "refs/remotes/$remoteName/master") standardOutput = output isIgnoreExitValue = true } - // Return the count if successful, otherwise a default of "1". return if (result.exitValue == 0 && output.toString().isNotBlank()) { output.toString().trim() } else { @@ -39,7 +41,6 @@ abstract class GitCommitCountValueSource : ValueSource { @get:Inject abstract val execOperations: ExecOperations @@ -202,7 +203,11 @@ abstract class GitCommitHashValueSource : ValueSource - - + android:height="512dp" + android:width="512dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + android:fillColor="#FFFFFFFF" + android:pathData="M39.4,23l-0.8,-4l-12.6,2.6l0,-13.6l-4,0l0,12.3l-13.9,-9l-2.2,3.4l15.2,9.8l-11.7,15.3l3.2,2.4l11.3,-14.8l8.4,12.7l3.4,-2.2l-8.4,-12.5z" /> + + + + + + diff --git a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ApiBadge.kt b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ApiBadge.kt index d34ecbe71..2964b41a5 100644 --- a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ApiBadge.kt +++ b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ApiBadge.kt @@ -2,39 +2,43 @@ package org.matrix.vector.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth 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.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.style.TextOverflow -/** - * `API 101` / `Xposed 93`, with the scale name small and quiet and the number carrying the colour. - * - * The scale name is context that rarely changes and repeats down every row; the number is the fact - * being checked, so it is the only part given weight and colour. A caller with no value to show - * passes `"?"` for [value] and `incompatible = true`, which keeps the badge the same shape while the - * missing number reads as missing rather than as a different kind of thing. - * - * Model-agnostic on purpose: Vector maps a module's declared API into ([label], [value]); LSPatch - * passes its own text. Both get one badge instead of two that drift apart. - */ @Composable fun ApiBadge(label: String, value: String, incompatible: Boolean = false) { val colors = MaterialTheme.colorScheme - Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(3.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom, + ) { Text( text = label, - style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp), + style = MaterialTheme.typography.labelSmall, // ← 统一用 labelMedium color = colors.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Clip, + ) + Text( + text = " ", + style = MaterialTheme.typography.labelSmall, + color = colors.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, ) Text( text = value, style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, + fontWeight = FontWeight.Black, color = if (incompatible) colors.error else colors.primary, + maxLines = 1, + overflow = TextOverflow.Clip, ) } -} +} \ No newline at end of file diff --git a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt index 367187a54..0339ee450 100644 --- a/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt +++ b/manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt @@ -131,24 +131,30 @@ fun ModuleRow( // leaving the list; a bare tap only reports state, since a one-tap toggle would fire whenever // a thumb brushed the list. Column( - modifier = - if (onIconClick != null) - Modifier.contextClickable(onClick = onIconClick, onLongClick = onIconLongClick) - else Modifier, - // Against the text, not centred over the badge: the badge below is wider than the icon, - // so centring left a gap between the icon and the edge the names all start from. - horizontalAlignment = Alignment.End, + modifier = Modifier.width(72.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - // Fixed at the icon's size whatever is drawn inside, so selecting a module cannot resize - // its row — a tick larger than the icon would grow this box and reflow the list. - Box(modifier = Modifier.size(ICON_SIZE), contentAlignment = Alignment.Center) { + // 图标 - 不裁剪,保持应用原生形状 + Box( + modifier = Modifier + .size(ICON_SIZE) + .clip(RoundedCornerShape(8.dp)) + .then( + if (onIconClick != null) + Modifier.contextClickable( + onClick = onIconClick, + onLongClick = onIconLongClick, + ) + else Modifier + ), + contentAlignment = Alignment.Center, + ) { icon() if (selected) { Box( - modifier = - Modifier.fillMaxSize() - .clip(CircleShape) - .background(colors.primary.copy(alpha = 0.85f)), + modifier = Modifier.fillMaxSize() + .clip(CircleShape) + .background(colors.primary.copy(alpha = 0.85f)), contentAlignment = Alignment.Center, ) { Icon( @@ -161,7 +167,12 @@ fun ModuleRow( } } Spacer(Modifier.height(6.dp)) - apiBadge() + Box( + modifier = Modifier.width(72.dp), + contentAlignment = Alignment.Center, + ) { + apiBadge() + } } Spacer(Modifier.width(16.dp)) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkUpdateRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkUpdateRepository.kt index 7a3c0c006..b82cdf75f 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkUpdateRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/FrameworkUpdateRepository.kt @@ -126,8 +126,5 @@ enum class ReleaseDirection { * row anywhere marked as installed. */ fun FrameworkUpdateState.divergesFrom(release: FrameworkRelease?): Boolean { - if (release == null || release.versionCode != installedVersionCode) return false - val mine = buildStamp(installedCommit ?: return false) - if (mine.commit == null || release.commit == null) return false - return !mine.isCommit(release.commit) + return false } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt index 94cb56565..9574aa6af 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt @@ -6,11 +6,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.CompositionLocalProvider -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import org.matrix.vector.manager.data.repository.LaunchShortcut import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.ui.navigation.DeepLink -import org.matrix.vector.manager.ui.screens.splash.SplashGate import org.matrix.vector.manager.ui.theme.LocalizedContent import org.matrix.vector.manager.ui.theme.LocalizedOverlay import org.matrix.vector.manager.ui.theme.VectorTheme @@ -27,9 +25,6 @@ import org.matrix.vector.ui.LocalDialogLocalizer class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - // Must precede super.onCreate. Handing off from the platform splash is what keeps an - // unthemed frame from appearing between the system splash and the Compose one. - val splash = installSplashScreen() enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -52,8 +47,6 @@ class MainActivity : ComponentActivity() { // Keep the platform splash up only until the first frame is ready to draw; the Compose // splash then plays and decides for itself when the daemon has been given long enough. - splash.setKeepOnScreenCondition { false } - // The launch intent can name where to open — the module a notification was about. // // Offered on every creation, including a restored one. Parasitically the zygisk hooker @@ -79,7 +72,7 @@ class MainActivity : ComponentActivity() { CompositionLocalProvider( LocalDialogLocalizer provides { content -> LocalizedOverlay(content) } ) { - VectorTheme { SplashGate { VectorApp() } } + VectorTheme { VectorApp() } } } } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt index b6d00feb4..227c6d2af 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeScreen.kt @@ -686,14 +686,14 @@ private fun QuarterHeadline(feed: CommunityFeed, windowChanged: Boolean) { // changed, so what is on screen was re-cut from disk and may not reach as far as the new // window does. It takes precedence over the other three because it is the newest fact and // the only actionable one. - if (windowChanged || feed.offline || feed.fromCache) { + // 只保留窗口变更和离线,删除"暂歇中" + if (windowChanged || feed.offline) { Spacer(Modifier.height(6.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Icon( when { windowChanged -> Icons.Rounded.Refresh - feed.offline -> Icons.Rounded.CloudOff - else -> Icons.Rounded.Bedtime + else -> Icons.Rounded.CloudOff }, contentDescription = null, modifier = Modifier.height(14.dp), @@ -707,8 +707,7 @@ private fun QuarterHeadline(feed: CommunityFeed, windowChanged: Boolean) { stringResource( when { windowChanged -> R.string.home_window_changed - feed.offline -> R.string.home_offline - else -> R.string.home_resting + else -> R.string.home_offline } ), style = MaterialTheme.typography.labelSmall, diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt index 725ff534d..9d7845d0b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeViewModel.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlin.random.Random import kotlinx.coroutines.launch +import kotlinx.coroutines.delay import org.matrix.vector.ipc.IManagerService import org.matrix.vector.manager.data.github.CommunityFeed import org.matrix.vector.manager.data.github.GitHubRepository @@ -347,25 +348,46 @@ class HomeViewModel( if (service != null) refreshToggles() } } - // Returning to Home is not a reason to talk to GitHub. The page renders from disk every - // time and only occasionally goes and checks — the window it shows changes a few times a - // week at most, and the user's battery and their share of an anonymous rate limit are worth - // more than redrawing identical rows. Pull-to-refresh is always there when they do want it. - // - // Opening the app *is* a reason, and the toss used to apply there too: four launches in five - // showed whatever was on disk, which after a while is a feed that has quietly stopped - // moving — and a cold start is exactly when it has had the longest to go stale. So the first - // Home of a process always checks, and the toss governs only the visits after it. Process - // scope rather than a timestamp because that is what "since the app was opened" means here: - // parasitically the host is `com.android.shell` and is killed constantly, but each of those - // deaths is also what makes the next arrival a first launch to the reader. - val firstThisProcess = !homeOpenedThisProcess - homeOpenedThisProcess = true - val checkNow = firstThisProcess || Random.nextFloat() >= FEED_PAUSE_PROBABILITY - refreshFeed( - if (checkNow) GitHubRepository.Freshness.Revalidate - else GitHubRepository.Freshness.Cached - ) + + // ============================================= + // Feed 加载逻辑:有缓存秒开,无缓存显示加载中 + // ============================================= + viewModelScope.launch { + val cached = github.load(GitHubRepository.Freshness.Cached) + if (cached.commits.isNotEmpty()) { + // 有缓存:秒开 + _feed.value = cached + delay(300) + try { + val fresh = github.load(GitHubRepository.Freshness.Revalidate) + if (fresh.commits.isNotEmpty()) { + _feed.value = fresh + } + } catch (_: Exception) { + // 网络失败,保留缓存 + } + _refreshing.value = false + _windowChanged.value = false + } else { + // 无缓存:显示加载中,立即联网 + _feed.value = CommunityFeed().copy(loaded = false) + _refreshing.value = true + try { + val fresh = github.load(GitHubRepository.Freshness.Revalidate) + if (fresh.commits.isNotEmpty()) { + _feed.value = fresh + } else { + _feed.value = CommunityFeed().copy(loaded = true) + } + } catch (_: Exception) { + // 网络失败,保持 loaded = false,用户可下拉刷新 + } finally { + _refreshing.value = false + _windowChanged.value = false + } + } + } + viewModelScope.launch { // drop(1): the value on subscription is the window already rendered. ServiceLocator.settings.activityWindowMonths.drop(1).collect { @@ -677,17 +699,6 @@ class HomeViewModel( val historyStalled: StateFlow = _exhausted.asStateFlow() companion object { - /** How often *returning* to Home leaves the feed on what is already on disk. */ - private const val FEED_PAUSE_PROBABILITY = 0.8f - - /** - * True once Home has been built in this process, whatever the feed did about it. - * - * On the companion because that is exactly the scope wanted — one per process, shared by - * every HomeViewModel a session builds, and gone when the host is killed. Volatile because - * `init` runs on whichever thread built the ViewModel. - */ - @Volatile private var homeOpenedThisProcess = false /** * How many times a day the badge has to be used before it stops explaining itself. diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/splash/SplashScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/splash/SplashScreen.kt index 88450db33..b4e32377a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/splash/SplashScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/splash/SplashScreen.kt @@ -1,6 +1,5 @@ package org.matrix.vector.manager.ui.screens.splash -import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -23,18 +22,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import kotlinx.coroutines.delay -import kotlinx.coroutines.withTimeoutOrNull import org.matrix.vector.manager.R -import org.matrix.vector.manager.di.ServiceLocator -import kotlinx.coroutines.flow.first -import org.matrix.vector.manager.logW /** How long the animation itself needs, so the statue is never cut off mid-fade. */ -private const val ANIMATION_MS = 800L - -/** The longest we wait on the daemon before showing the UI anyway, in its "not activated" state. */ -private const val DAEMON_TIMEOUT_MS = 2_500L +private const val ANIMATION_MS = 700L /** * The Winged Victory, fading and scaling in — Vector, from *Victoria*. @@ -46,25 +37,6 @@ private const val DAEMON_TIMEOUT_MS = 2_500L * [ANIMATION_MS] is then spent after the handshake rather than overlapped with it, so a binder that * resolves instantly still leaves the fade its full length instead of a flash of half-drawn artwork. */ -@Composable -fun SplashGate(content: @Composable () -> Unit) { - var ready by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - // The handshake first, under a ceiling; then the artwork's own duration. - val bound = - withTimeoutOrNull(DAEMON_TIMEOUT_MS) { ServiceLocator.service.first { it != null } } - if (bound == null) { - logW("splash: no daemon binder after ${DAEMON_TIMEOUT_MS}ms, continuing unactivated") - } - delay(ANIMATION_MS) - ready = true - } - - Crossfade(targetState = ready, animationSpec = tween(320), label = "splashHandoff") { done -> - if (done) content() else WingedVictory() - } -} /** The splash artwork, also summoned by the header's easter egg. */ @Composable @@ -101,7 +73,7 @@ fun WingedVictory() { // The drawable is a 108dp square whose figure runs nearly the full height of its // viewport, so it is given 92% of both dimensions and fitted inside — which keeps its // proportions in portrait and landscape without stretching or clipping. - modifier = Modifier.fillMaxSize(0.92f).scale(scale).alpha(alpha), + modifier = Modifier.fillMaxSize(0.82f).scale(scale).alpha(alpha), ) } } diff --git a/manager/src/main/res/drawable/ic_winged_victory.xml b/manager/src/main/res/drawable/ic_winged_victory.xml index 193431ad9..ecec36271 100644 --- a/manager/src/main/res/drawable/ic_winged_victory.xml +++ b/manager/src/main/res/drawable/ic_winged_victory.xml @@ -1,464 +1,28 @@ - + android:height="512dp" + android:width="512dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + android:fillColor="#FFCFD8DC" + android:pathData="M39.4,23l-0.8,-4l-12.6,2.6l0,-13.6l-4,0l0,12.3l-13.9,-9l-2.2,3.4l15.2,9.8l-11.7,15.3l3.2,2.4l11.3,-14.8l8.4,12.7l3.4,-2.2l-8.4,-12.5z" /> + android:fillColor="#FF3F51B5" + android:pathData="M24,24m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0" /> + android:fillColor="#FF00BCD4" + android:pathData="M24,8m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" /> + android:fillColor="#FF00BCD4" + android:pathData="M39,21m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" /> + android:fillColor="#FF00BCD4" + android:pathData="M7,13m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" /> + android:fillColor="#FF00BCD4" + android:pathData="M11,41m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:fillColor="#FF00BCD4" + android:pathData="M34,39m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0" /> diff --git a/manager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/manager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 935333819..ac44568e4 100644 --- a/manager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/manager/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -4,11 +4,11 @@ + android:inset="22%" /> + android:inset="22%" />